mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-01-03 22:03:06 +00:00
feat: adding rpc interfaces into sequencer and node
This commit is contained in:
parent
c95ed90c9a
commit
42cfac8a74
@ -14,7 +14,8 @@ members = [
|
|||||||
"mempool",
|
"mempool",
|
||||||
"zkvm",
|
"zkvm",
|
||||||
"node_core",
|
"node_core",
|
||||||
"sequencer_core",
|
"sequencer_core",
|
||||||
|
"rpc_primitives",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -23,6 +24,9 @@ num_cpus = "1.13.1"
|
|||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
openssl-probe = { version = "0.1.2" }
|
openssl-probe = { version = "0.1.2" }
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
|
actix = "0.13.0"
|
||||||
|
actix-cors = "0.6.1"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add consensus module
|
//ToDo: Add consensus module
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add mempool module
|
//ToDo: Add mempool module
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add networking module
|
//ToDo: Add networking module
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add node_core module
|
//ToDo: Add node_core module
|
||||||
|
|||||||
@ -9,6 +9,9 @@ serde_json.workspace = true
|
|||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
actix.workspace = true
|
||||||
|
actix-cors.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
|
|
||||||
@ -34,4 +37,7 @@ path = "../vm"
|
|||||||
path = "../zkvm"
|
path = "../zkvm"
|
||||||
|
|
||||||
[dependencies.node_core]
|
[dependencies.node_core]
|
||||||
path = "../node_core"
|
path = "../node_core"
|
||||||
|
|
||||||
|
[dependencies.rpc_primitives]
|
||||||
|
path = "../rpc_primitives"
|
||||||
@ -1 +1,39 @@
|
|||||||
//ToDo: Add node_rpc module
|
pub mod net_utils;
|
||||||
|
pub mod process;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use rpc_primitives::{
|
||||||
|
errors::{RpcError, RpcErrorKind},
|
||||||
|
RpcPollingConfig,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub use net_utils::*;
|
||||||
|
|
||||||
|
use self::types::err_rpc::RpcErr;
|
||||||
|
|
||||||
|
//ToDo: Add necessary fields
|
||||||
|
pub struct JsonHandler {
|
||||||
|
pub polling_config: RpcPollingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn respond<T: Serialize>(val: T) -> Result<Value, RpcErr> {
|
||||||
|
Ok(serde_json::to_value(val)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError {
|
||||||
|
let mut content: Option<Value> = None;
|
||||||
|
if err.error_struct.is_some() {
|
||||||
|
content = match err.error_struct.clone().unwrap() {
|
||||||
|
RpcErrorKind::HandlerError(val) | RpcErrorKind::InternalError(val) => Some(val),
|
||||||
|
RpcErrorKind::RequestValidationError(vall) => Some(serde_json::to_value(vall).unwrap()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
RpcError {
|
||||||
|
error_struct: None,
|
||||||
|
code: err.code,
|
||||||
|
message: err.message,
|
||||||
|
data: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
64
node_rpc/src/net_utils.rs
Normal file
64
node_rpc/src/net_utils.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{http, middleware, web, App, Error as HttpError, HttpResponse, HttpServer};
|
||||||
|
use futures::Future;
|
||||||
|
use futures::FutureExt;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use rpc_primitives::message::Message;
|
||||||
|
use rpc_primitives::RpcConfig;
|
||||||
|
|
||||||
|
use super::JsonHandler;
|
||||||
|
|
||||||
|
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
|
fn rpc_handler(
|
||||||
|
message: web::Json<Message>,
|
||||||
|
handler: web::Data<JsonHandler>,
|
||||||
|
) -> impl Future<Output = Result<HttpResponse, HttpError>> {
|
||||||
|
let response = async move {
|
||||||
|
let message = handler.process(message.0).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(&message))
|
||||||
|
};
|
||||||
|
response.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cors(cors_allowed_origins: &[String]) -> Cors {
|
||||||
|
let mut cors = Cors::permissive();
|
||||||
|
if cors_allowed_origins != ["*".to_string()] {
|
||||||
|
for origin in cors_allowed_origins {
|
||||||
|
cors = cors.allowed_origin(origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cors.allowed_methods(vec!["GET", "POST"])
|
||||||
|
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||||
|
.allowed_header(http::header::CONTENT_TYPE)
|
||||||
|
.max_age(3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new_http_server(config: RpcConfig) -> io::Result<actix_web::dev::Server> {
|
||||||
|
let RpcConfig {
|
||||||
|
addr,
|
||||||
|
cors_allowed_origins,
|
||||||
|
polling_config,
|
||||||
|
limits_config,
|
||||||
|
} = config;
|
||||||
|
info!(target:"network", "Starting http server at {}", addr);
|
||||||
|
let handler = web::Data::new(JsonHandler { polling_config });
|
||||||
|
|
||||||
|
// HTTP server
|
||||||
|
Ok(HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(get_cors(&cors_allowed_origins))
|
||||||
|
.app_data(handler.clone())
|
||||||
|
.app_data(web::JsonConfig::default().limit(limits_config.json_payload_max_size))
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
.service(web::resource("/").route(web::post().to(rpc_handler)))
|
||||||
|
})
|
||||||
|
.bind(addr)?
|
||||||
|
.shutdown_timeout(SHUTDOWN_TIMEOUT_SECS)
|
||||||
|
.disable_signals()
|
||||||
|
.run())
|
||||||
|
}
|
||||||
53
node_rpc/src/process.rs
Normal file
53
node_rpc/src/process.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use actix_web::Error as HttpError;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use rpc_primitives::{
|
||||||
|
errors::RpcError,
|
||||||
|
message::{Message, Request},
|
||||||
|
parser::RpcRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
rpc_error_responce_inverter,
|
||||||
|
types::rpc_structs::{HelloRequest, HelloResponse},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{respond, types::err_rpc::RpcErr, JsonHandler};
|
||||||
|
|
||||||
|
impl JsonHandler {
|
||||||
|
pub async fn process(&self, message: Message) -> Result<Message, HttpError> {
|
||||||
|
let id = message.id();
|
||||||
|
if let Message::Request(request) = message {
|
||||||
|
let message_inner = self
|
||||||
|
.process_request_internal(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.0)
|
||||||
|
.map_err(rpc_error_responce_inverter);
|
||||||
|
Ok(Message::response(id, message_inner))
|
||||||
|
} else {
|
||||||
|
Ok(Message::error(RpcError::parse_error(
|
||||||
|
"JSON RPC Request format was expected".to_owned(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
///Example of request processing
|
||||||
|
async fn process_temp_hello(&self, request: Request) -> Result<Value, RpcErr> {
|
||||||
|
let _hello_request = HelloRequest::parse(Some(request.params))?;
|
||||||
|
|
||||||
|
let helperstruct = HelloResponse {
|
||||||
|
greeting: "HELLO_FROM_NODE".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
respond(helperstruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_request_internal(&self, request: Request) -> Result<Value, RpcErr> {
|
||||||
|
match request.method.as_ref() {
|
||||||
|
//Todo : Add handling of more JSON RPC methods
|
||||||
|
"hello" => self.process_temp_hello(request).await,
|
||||||
|
_ => Err(RpcErr(RpcError::method_not_found(request.method))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
node_rpc/src/types/err_rpc.rs
Normal file
47
node_rpc/src/types/err_rpc.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use rpc_primitives::errors::{RpcError, RpcParseError};
|
||||||
|
|
||||||
|
pub struct RpcErr(pub RpcError);
|
||||||
|
|
||||||
|
pub type RpcErrInternal = anyhow::Error;
|
||||||
|
|
||||||
|
pub trait RpcErrKind: 'static {
|
||||||
|
fn into_rpc_err(self) -> RpcError;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: RpcErrKind> From<T> for RpcErr {
|
||||||
|
fn from(e: T) -> Self {
|
||||||
|
Self(e.into_rpc_err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! standard_rpc_err_kind {
|
||||||
|
($type_name:path) => {
|
||||||
|
impl RpcErrKind for $type_name {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
standard_rpc_err_kind!(RpcError);
|
||||||
|
standard_rpc_err_kind!(RpcParseError);
|
||||||
|
|
||||||
|
impl RpcErrKind for serde_json::Error {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
RpcError::serialization_error(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcErrKind for RpcErrInternal {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
RpcError::new_internal_error(None, &format!("{self:#?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn from_rpc_err_into_anyhow_err(rpc_err: RpcError) -> anyhow::Error {
|
||||||
|
debug!("Rpc error cast to anyhow error : err {rpc_err:?}");
|
||||||
|
anyhow::anyhow!(format!("{rpc_err:#?}"))
|
||||||
|
}
|
||||||
3
node_rpc/src/types/mod.rs
Normal file
3
node_rpc/src/types/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod err_rpc;
|
||||||
|
pub mod parse;
|
||||||
|
pub mod rpc_structs;
|
||||||
1
node_rpc/src/types/parse.rs
Normal file
1
node_rpc/src/types/parse.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
16
node_rpc/src/types/rpc_structs.rs
Normal file
16
node_rpc/src/types/rpc_structs.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use rpc_primitives::errors::RpcParseError;
|
||||||
|
use rpc_primitives::parse_request;
|
||||||
|
use rpc_primitives::parser::parse_params;
|
||||||
|
use rpc_primitives::parser::RpcRequest;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct HelloRequest {}
|
||||||
|
|
||||||
|
parse_request!(HelloRequest);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct HelloResponse {
|
||||||
|
pub greeting: String,
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ serde_json.workspace = true
|
|||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
actix.workspace = true
|
||||||
|
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@ -38,4 +39,7 @@ path = "../zkvm"
|
|||||||
path = "../node_rpc"
|
path = "../node_rpc"
|
||||||
|
|
||||||
[dependencies.node_core]
|
[dependencies.node_core]
|
||||||
path = "../node_core"
|
path = "../node_core"
|
||||||
|
|
||||||
|
[dependencies.rpc_primitives]
|
||||||
|
path = "../rpc_primitives"
|
||||||
17
node_runner/src/lib.rs
Normal file
17
node_runner/src/lib.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
use node_rpc::new_http_server;
|
||||||
|
use rpc_primitives::RpcConfig;
|
||||||
|
|
||||||
|
pub async fn main_runner() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let http_server = new_http_server(RpcConfig::default())?;
|
||||||
|
info!("HTTP server started");
|
||||||
|
let _http_server_handle = http_server.handle();
|
||||||
|
tokio::spawn(http_server);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
//ToDo: Insert activity into main loop
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,16 @@
|
|||||||
//ToDo: Add node_runner module
|
use anyhow::Result;
|
||||||
|
|
||||||
fn main() {
|
use node_runner::main_runner;
|
||||||
println!("Hello, world!");
|
|
||||||
|
pub const NUM_THREADS: usize = 4;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
actix::System::with_tokio_rt(|| {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(NUM_THREADS)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.block_on(main_runner())
|
||||||
}
|
}
|
||||||
|
|||||||
11
rpc_primitives/Cargo.toml
Normal file
11
rpc_primitives/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "rpc_primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
185
rpc_primitives/src/errors.rs
Normal file
185
rpc_primitives/src/errors.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
use serde_json::{to_value, Value};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct RpcParseError(pub String);
|
||||||
|
|
||||||
|
/// This struct may be returned from JSON RPC server in case of error
|
||||||
|
/// It is expected that that this struct has impls From<_> all other RPC errors
|
||||||
|
/// like [`RpcBlockError`](crate::types::blocks::RpcBlockError)
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RpcError {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub error_struct: Option<RpcErrorKind>,
|
||||||
|
/// Deprecated please use the `error_struct` instead
|
||||||
|
pub code: i64,
|
||||||
|
/// Deprecated please use the `error_struct` instead
|
||||||
|
pub message: String,
|
||||||
|
/// Deprecated please use the `error_struct` instead
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub data: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(tag = "name", content = "cause", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum RpcErrorKind {
|
||||||
|
RequestValidationError(RpcRequestValidationErrorKind),
|
||||||
|
HandlerError(Value),
|
||||||
|
InternalError(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(tag = "name", content = "info", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum RpcRequestValidationErrorKind {
|
||||||
|
MethodNotFound { method_name: String },
|
||||||
|
ParseError { error_message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A general Server Error
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum ServerError {
|
||||||
|
Timeout,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcError {
|
||||||
|
/// A generic constructor.
|
||||||
|
///
|
||||||
|
/// Mostly for completeness, doesn't do anything but filling in the corresponding fields.
|
||||||
|
pub fn new(code: i64, message: String, data: Option<Value>) -> Self {
|
||||||
|
RpcError {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
data,
|
||||||
|
error_struct: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an Invalid Param error.
|
||||||
|
pub fn invalid_params(data: impl serde::Serialize) -> Self {
|
||||||
|
let value = match to_value(data) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
return Self::server_error(Some(format!(
|
||||||
|
"Failed to serialize invalid parameters error: {:?}",
|
||||||
|
err.to_string()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
RpcError::new(-32_602, "Invalid params".to_owned(), Some(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a server error.
|
||||||
|
pub fn server_error<E: serde::Serialize>(e: Option<E>) -> Self {
|
||||||
|
RpcError::new(
|
||||||
|
-32_000,
|
||||||
|
"Server error".to_owned(),
|
||||||
|
e.map(|v| to_value(v).expect("Must be representable in JSON")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a parse error.
|
||||||
|
pub fn parse_error(e: String) -> Self {
|
||||||
|
RpcError {
|
||||||
|
code: -32_700,
|
||||||
|
message: "Parse error".to_owned(),
|
||||||
|
data: Some(Value::String(e.clone())),
|
||||||
|
error_struct: Some(RpcErrorKind::RequestValidationError(
|
||||||
|
RpcRequestValidationErrorKind::ParseError { error_message: e },
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialization_error(e: &str) -> Self {
|
||||||
|
RpcError::new_internal_error(Some(Value::String(e.to_owned())), e)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to define extract `INTERNAL_ERROR` in separate `RpcErrorKind`
|
||||||
|
/// Returns `HANDLER_ERROR` if the error is not internal one
|
||||||
|
pub fn new_internal_or_handler_error(error_data: Option<Value>, error_struct: Value) -> Self {
|
||||||
|
if error_struct["name"] == "INTERNAL_ERROR" {
|
||||||
|
let error_message = match error_struct["info"].get("error_message") {
|
||||||
|
Some(Value::String(error_message)) => error_message.as_str(),
|
||||||
|
_ => "InternalError happened during serializing InternalError",
|
||||||
|
};
|
||||||
|
Self::new_internal_error(error_data, error_message)
|
||||||
|
} else {
|
||||||
|
Self::new_handler_error(error_data, error_struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_internal_error(error_data: Option<Value>, info: &str) -> Self {
|
||||||
|
RpcError {
|
||||||
|
code: -32_000,
|
||||||
|
message: "Server error".to_owned(),
|
||||||
|
data: error_data,
|
||||||
|
error_struct: Some(RpcErrorKind::InternalError(serde_json::json!({
|
||||||
|
"name": "INTERNAL_ERROR",
|
||||||
|
"info": serde_json::json!({"error_message": info})
|
||||||
|
}))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_handler_error(error_data: Option<Value>, error_struct: Value) -> Self {
|
||||||
|
RpcError {
|
||||||
|
code: -32_000,
|
||||||
|
message: "Server error".to_owned(),
|
||||||
|
data: error_data,
|
||||||
|
error_struct: Some(RpcErrorKind::HandlerError(error_struct)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a method not found error.
|
||||||
|
pub fn method_not_found(method: String) -> Self {
|
||||||
|
RpcError {
|
||||||
|
code: -32_601,
|
||||||
|
message: "Method not found".to_owned(),
|
||||||
|
data: Some(Value::String(method.clone())),
|
||||||
|
error_struct: Some(RpcErrorKind::RequestValidationError(
|
||||||
|
RpcRequestValidationErrorKind::MethodNotFound {
|
||||||
|
method_name: method,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RpcError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RpcParseError> for RpcError {
|
||||||
|
fn from(parse_error: RpcParseError) -> Self {
|
||||||
|
Self::parse_error(parse_error.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::convert::Infallible> for RpcError {
|
||||||
|
fn from(_: std::convert::Infallible) -> Self {
|
||||||
|
unsafe { core::hint::unreachable_unchecked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ServerError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ServerError::Timeout => write!(f, "ServerError: Timeout"),
|
||||||
|
ServerError::Closed => write!(f, "ServerError: Closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServerError> for RpcError {
|
||||||
|
fn from(e: ServerError) -> RpcError {
|
||||||
|
let error_data = match to_value(&e) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_err) => {
|
||||||
|
return RpcError::new_internal_error(None, "Failed to serialize ServerError")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
RpcError::new_internal_error(Some(error_data), e.to_string().as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
72
rpc_primitives/src/lib.rs
Normal file
72
rpc_primitives/src/lib.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod errors;
|
||||||
|
pub mod message;
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub struct RpcPollingConfig {
|
||||||
|
pub polling_interval: Duration,
|
||||||
|
pub polling_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RpcPollingConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
polling_interval: Duration::from_millis(500),
|
||||||
|
polling_timeout: Duration::from_secs(10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct RpcLimitsConfig {
|
||||||
|
/// Maximum byte size of the json payload.
|
||||||
|
pub json_payload_max_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RpcLimitsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
json_payload_max_size: 10 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct RpcConfig {
|
||||||
|
pub addr: String,
|
||||||
|
pub cors_allowed_origins: Vec<String>,
|
||||||
|
pub polling_config: RpcPollingConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub limits_config: RpcLimitsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RpcConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
RpcConfig {
|
||||||
|
addr: "0.0.0.0:3040".to_owned(),
|
||||||
|
cors_allowed_origins: vec!["*".to_owned()],
|
||||||
|
polling_config: RpcPollingConfig::default(),
|
||||||
|
limits_config: RpcLimitsConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcConfig {
|
||||||
|
pub fn new(addr: &str) -> Self {
|
||||||
|
RpcConfig {
|
||||||
|
addr: addr.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_port(port: u16) -> Self {
|
||||||
|
RpcConfig {
|
||||||
|
addr: format!("0.0.0.0:{port}"),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
551
rpc_primitives/src/message.rs
Normal file
551
rpc_primitives/src/message.rs
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
// Copyright 2017 tokio-jsonrpc Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
|
||||||
|
//! JSON-RPC 2.0 messages.
|
||||||
|
//!
|
||||||
|
//! The main entrypoint here is the [Message](enum.Message.html). The others are just building
|
||||||
|
//! blocks and you should generally work with `Message` instead.
|
||||||
|
use serde::de::{Deserializer, Error, Unexpected, Visitor};
|
||||||
|
use serde::ser::{SerializeStruct, Serializer};
|
||||||
|
use serde_json::{Result as JsonResult, Value};
|
||||||
|
use std::fmt::{Formatter, Result as FmtResult};
|
||||||
|
|
||||||
|
use super::errors::RpcError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct Version;
|
||||||
|
|
||||||
|
impl serde::Serialize for Version {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str("2.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for Version {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct VersionVisitor;
|
||||||
|
impl<'de> Visitor<'de> for VersionVisitor {
|
||||||
|
type Value = Version;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
formatter.write_str("a version string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: Error>(self, value: &str) -> Result<Version, E> {
|
||||||
|
match value {
|
||||||
|
"2.0" => Ok(Version),
|
||||||
|
_ => Err(E::invalid_value(Unexpected::Str(value), &"value 2.0")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializer.deserialize_str(VersionVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An RPC request.
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
pub method: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||||
|
pub params: Value,
|
||||||
|
pub id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Answer the request with a (positive) reply.
|
||||||
|
///
|
||||||
|
/// The ID is taken from the request.
|
||||||
|
pub fn reply(&self, reply: Value) -> Message {
|
||||||
|
Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Ok(reply),
|
||||||
|
id: self.id.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Answer the request with an error.
|
||||||
|
pub fn error(&self, error: RpcError) -> Message {
|
||||||
|
Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Err(error),
|
||||||
|
id: self.id.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A response to an RPC.
|
||||||
|
///
|
||||||
|
/// It is created by the methods on [Request](struct.Request.html).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
pub result: Result<Value, RpcError>,
|
||||||
|
pub id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for Response {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut sub = serializer.serialize_struct("Response", 3)?;
|
||||||
|
sub.serialize_field("jsonrpc", &self.jsonrpc)?;
|
||||||
|
match self.result {
|
||||||
|
Ok(ref value) => sub.serialize_field("result", value),
|
||||||
|
Err(ref err) => sub.serialize_field("error", err),
|
||||||
|
}?;
|
||||||
|
sub.serialize_field("id", &self.id)?;
|
||||||
|
sub.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializer for `Option<Value>` that produces `Some(Value::Null)`.
|
||||||
|
///
|
||||||
|
/// The usual one produces None in that case. But we need to know the difference between
|
||||||
|
/// `{x: null}` and `{}`.
|
||||||
|
fn some_value<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Value>, D::Error> {
|
||||||
|
serde::Deserialize::deserialize(deserializer).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper trick for deserialization.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct WireResponse {
|
||||||
|
// It is actually used to eat and sanity check the deserialized text
|
||||||
|
#[allow(dead_code)]
|
||||||
|
jsonrpc: Version,
|
||||||
|
// Make sure we accept null as Some(Value::Null), instead of going to None
|
||||||
|
#[serde(default, deserialize_with = "some_value")]
|
||||||
|
result: Option<Value>,
|
||||||
|
error: Option<RpcError>,
|
||||||
|
id: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementing deserialize is hard. We sidestep the difficulty by deserializing a similar
|
||||||
|
// structure that directly corresponds to whatever is on the wire and then convert it to our more
|
||||||
|
// convenient representation.
|
||||||
|
impl<'de> serde::Deserialize<'de> for Response {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let wr: WireResponse = serde::Deserialize::deserialize(deserializer)?;
|
||||||
|
let result = match (wr.result, wr.error) {
|
||||||
|
(Some(res), None) => Ok(res),
|
||||||
|
(None, Some(err)) => Err(err),
|
||||||
|
_ => {
|
||||||
|
let err = D::Error::custom("Either 'error' or 'result' is expected, but not both");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result,
|
||||||
|
id: wr.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A notification (doesn't expect an answer).
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Notification {
|
||||||
|
jsonrpc: Version,
|
||||||
|
pub method: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||||
|
pub params: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One message of the JSON RPC protocol.
|
||||||
|
///
|
||||||
|
/// One message, directly mapped from the structures of the protocol. See the
|
||||||
|
/// [specification](http://www.jsonrpc.org/specification) for more details.
|
||||||
|
///
|
||||||
|
/// Since the protocol allows one endpoint to be both client and server at the same time, the
|
||||||
|
/// message can decode and encode both directions of the protocol.
|
||||||
|
///
|
||||||
|
/// The `Batch` variant is supposed to be created directly, without a constructor.
|
||||||
|
///
|
||||||
|
/// The `UnmatchedSub` variant is used when a request is an array and some of the subrequests
|
||||||
|
/// aren't recognized as valid json rpc 2.0 messages. This is never returned as a top-level
|
||||||
|
/// element, it is returned as `Err(Broken::Unmatched)`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Message {
|
||||||
|
/// An RPC request.
|
||||||
|
Request(Request),
|
||||||
|
/// A response to a Request.
|
||||||
|
Response(Response),
|
||||||
|
/// A notification.
|
||||||
|
Notification(Notification),
|
||||||
|
/// A batch of more requests or responses.
|
||||||
|
///
|
||||||
|
/// The protocol allows bundling multiple requests, notifications or responses to a single
|
||||||
|
/// message.
|
||||||
|
///
|
||||||
|
/// This variant has no direct constructor and is expected to be constructed manually.
|
||||||
|
Batch(Vec<Message>),
|
||||||
|
/// An unmatched sub entry in a `Batch`.
|
||||||
|
///
|
||||||
|
/// When there's a `Batch` and an element doesn't comform to the JSONRPC 2.0 format, that one
|
||||||
|
/// is represented by this. This is never produced as a top-level value when parsing, the
|
||||||
|
/// `Err(Broken::Unmatched)` is used instead. It is not possible to serialize.
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
UnmatchedSub(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// A constructor for a request.
|
||||||
|
///
|
||||||
|
/// The ID is auto-set to dontcare.
|
||||||
|
pub fn request(method: String, params: Value) -> Self {
|
||||||
|
let id = Value::from("dontcare");
|
||||||
|
Message::Request(Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Create a top-level error (without an ID).
|
||||||
|
pub fn error(error: RpcError) -> Self {
|
||||||
|
Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Err(error),
|
||||||
|
id: Value::Null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// A constructor for a notification.
|
||||||
|
pub fn notification(method: String, params: Value) -> Self {
|
||||||
|
Message::Notification(Notification {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// A constructor for a response.
|
||||||
|
pub fn response(id: Value, result: Result<Value, RpcError>) -> Self {
|
||||||
|
Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns id or Null if there is no id.
|
||||||
|
pub fn id(&self) -> Value {
|
||||||
|
match self {
|
||||||
|
Message::Request(req) => req.id.clone(),
|
||||||
|
_ => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A broken message.
|
||||||
|
///
|
||||||
|
/// Protocol-level errors.
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Broken {
|
||||||
|
/// It was valid JSON, but doesn't match the form of a JSONRPC 2.0 message.
|
||||||
|
Unmatched(Value),
|
||||||
|
/// Invalid JSON.
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
SyntaxError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Broken {
|
||||||
|
/// Generate an appropriate error message.
|
||||||
|
///
|
||||||
|
/// The error message for these things are specified in the RFC, so this just creates an error
|
||||||
|
/// with the right values.
|
||||||
|
pub fn reply(&self) -> Message {
|
||||||
|
match *self {
|
||||||
|
Broken::Unmatched(_) => Message::error(RpcError::parse_error(
|
||||||
|
"JSON RPC Request format was expected".to_owned(),
|
||||||
|
)),
|
||||||
|
Broken::SyntaxError(ref e) => Message::error(RpcError::parse_error(e.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trick to easily deserialize and detect valid JSON, but invalid Message.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WireMessage {
|
||||||
|
Message(Message),
|
||||||
|
Broken(Broken),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decoded_to_parsed(res: JsonResult<WireMessage>) -> Parsed {
|
||||||
|
match res {
|
||||||
|
Ok(WireMessage::Message(Message::UnmatchedSub(value))) => Err(Broken::Unmatched(value)),
|
||||||
|
Ok(WireMessage::Message(m)) => Ok(m),
|
||||||
|
Ok(WireMessage::Broken(b)) => Err(b),
|
||||||
|
Err(e) => Err(Broken::SyntaxError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Parsed = Result<Message, Broken>;
|
||||||
|
|
||||||
|
/// Read a [Message](enum.Message.html) from a slice.
|
||||||
|
///
|
||||||
|
/// Invalid JSON or JSONRPC messages are reported as [Broken](enum.Broken.html).
|
||||||
|
pub fn from_slice(s: &[u8]) -> Parsed {
|
||||||
|
decoded_to_parsed(::serde_json::de::from_slice(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a [Message](enum.Message.html) from a string.
|
||||||
|
///
|
||||||
|
/// Invalid JSON or JSONRPC messages are reported as [Broken](enum.Broken.html).
|
||||||
|
pub fn from_str(s: &str) -> Parsed {
|
||||||
|
from_slice(s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for String {
|
||||||
|
fn from(val: Message) -> Self {
|
||||||
|
::serde_json::ser::to_string(&val).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for Vec<u8> {
|
||||||
|
fn from(val: Message) -> Self {
|
||||||
|
::serde_json::ser::to_vec(&val).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::de::from_slice;
|
||||||
|
use serde_json::json;
|
||||||
|
use serde_json::ser::to_vec;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Test serialization and deserialization of the Message
|
||||||
|
///
|
||||||
|
/// We first deserialize it from a string. That way we check deserialization works.
|
||||||
|
/// But since serialization doesn't have to produce the exact same result (order, spaces, …),
|
||||||
|
/// we then serialize and deserialize the thing again and check it matches.
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn message_serde() {
|
||||||
|
// A helper for running one message test
|
||||||
|
fn one(input: &str, expected: &Message) {
|
||||||
|
let parsed: Message = from_str(input).unwrap();
|
||||||
|
assert_eq!(*expected, parsed);
|
||||||
|
let serialized = to_vec(&parsed).unwrap();
|
||||||
|
let deserialized: Message = from_slice(&serialized).unwrap();
|
||||||
|
assert_eq!(parsed, deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request without parameters
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "method": "call", "id": 1}"#,
|
||||||
|
&Message::Request(Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "call".to_owned(),
|
||||||
|
params: Value::Null,
|
||||||
|
id: json!(1),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// A request with parameters
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "method": "call", "params": [1, 2, 3], "id": 2}"#,
|
||||||
|
&Message::Request(Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "call".to_owned(),
|
||||||
|
params: json!([1, 2, 3]),
|
||||||
|
id: json!(2),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// A notification (with parameters)
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "method": "notif", "params": {"x": "y"}}"#,
|
||||||
|
&Message::Notification(Notification {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "notif".to_owned(),
|
||||||
|
params: json!({"x": "y"}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// A successful response
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "result": 42, "id": 3}"#,
|
||||||
|
&Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Ok(json!(42)),
|
||||||
|
id: json!(3),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// A successful response
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "result": null, "id": 3}"#,
|
||||||
|
&Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Ok(Value::Null),
|
||||||
|
id: json!(3),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// An error
|
||||||
|
one(
|
||||||
|
r#"{"jsonrpc": "2.0", "error": {"code": 42, "message": "Wrong!"}, "id": null}"#,
|
||||||
|
&Message::Response(Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Err(RpcError::new(42, "Wrong!".to_owned(), None)),
|
||||||
|
id: Value::Null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// A batch
|
||||||
|
one(
|
||||||
|
r#"[
|
||||||
|
{"jsonrpc": "2.0", "method": "notif"},
|
||||||
|
{"jsonrpc": "2.0", "method": "call", "id": 42}
|
||||||
|
]"#,
|
||||||
|
&Message::Batch(vec![
|
||||||
|
Message::Notification(Notification {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "notif".to_owned(),
|
||||||
|
params: Value::Null,
|
||||||
|
}),
|
||||||
|
Message::Request(Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "call".to_owned(),
|
||||||
|
params: Value::Null,
|
||||||
|
id: json!(42),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// Some handling of broken messages inside a batch
|
||||||
|
let parsed = from_str(
|
||||||
|
r#"[
|
||||||
|
{"jsonrpc": "2.0", "method": "notif"},
|
||||||
|
{"jsonrpc": "2.0", "method": "call", "id": 42},
|
||||||
|
true
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Message::Batch(vec![
|
||||||
|
Message::Notification(Notification {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "notif".to_owned(),
|
||||||
|
params: Value::Null,
|
||||||
|
}),
|
||||||
|
Message::Request(Request {
|
||||||
|
jsonrpc: Version,
|
||||||
|
method: "call".to_owned(),
|
||||||
|
params: Value::Null,
|
||||||
|
id: json!(42),
|
||||||
|
}),
|
||||||
|
Message::UnmatchedSub(Value::Bool(true)),
|
||||||
|
]),
|
||||||
|
parsed
|
||||||
|
);
|
||||||
|
to_vec(&Message::UnmatchedSub(Value::Null)).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper for the `broken` test.
|
||||||
|
///
|
||||||
|
/// Check that the given JSON string parses, but is not recognized as a valid RPC message.
|
||||||
|
|
||||||
|
/// Test things that are almost but not entirely JSONRPC are rejected
|
||||||
|
///
|
||||||
|
/// The reject is done by returning it as Unmatched.
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::panic)]
|
||||||
|
fn broken() {
|
||||||
|
// A helper with one test
|
||||||
|
fn one(input: &str) {
|
||||||
|
let msg = from_str(input);
|
||||||
|
match msg {
|
||||||
|
Err(Broken::Unmatched(_)) => (),
|
||||||
|
_ => panic!("{input} recognized as an RPC message: {msg:?}!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing the version
|
||||||
|
one(r#"{"method": "notif"}"#);
|
||||||
|
// Wrong version
|
||||||
|
one(r#"{"jsonrpc": 2.0, "method": "notif"}"#);
|
||||||
|
// A response with both result and error
|
||||||
|
one(r#"{"jsonrpc": "2.0", "result": 42, "error": {"code": 42, "message": "!"}, "id": 1}"#);
|
||||||
|
// A response without an id
|
||||||
|
one(r#"{"jsonrpc": "2.0", "result": 42}"#);
|
||||||
|
// An extra field
|
||||||
|
one(r#"{"jsonrpc": "2.0", "method": "weird", "params": 42, "others": 43, "id": 2}"#);
|
||||||
|
// Something completely different
|
||||||
|
one(r#"{"x": [1, 2, 3]}"#);
|
||||||
|
|
||||||
|
match from_str(r#"{]"#) {
|
||||||
|
Err(Broken::SyntaxError(_)) => (),
|
||||||
|
other => panic!("Something unexpected: {other:?}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test some non-trivial aspects of the constructors
|
||||||
|
///
|
||||||
|
/// This doesn't have a full coverage, because there's not much to actually test there.
|
||||||
|
/// Most of it is related to the ids.
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::panic)]
|
||||||
|
#[ignore]
|
||||||
|
fn constructors() {
|
||||||
|
let msg1 = Message::request("call".to_owned(), json!([1, 2, 3]));
|
||||||
|
let msg2 = Message::request("call".to_owned(), json!([1, 2, 3]));
|
||||||
|
// They differ, even when created with the same parameters
|
||||||
|
assert_ne!(msg1, msg2);
|
||||||
|
// And, specifically, they differ in the ID's
|
||||||
|
let (req1, req2) = if let (Message::Request(req1), Message::Request(req2)) = (msg1, msg2) {
|
||||||
|
assert_ne!(req1.id, req2.id);
|
||||||
|
assert!(req1.id.is_string());
|
||||||
|
assert!(req2.id.is_string());
|
||||||
|
(req1, req2)
|
||||||
|
} else {
|
||||||
|
panic!("Non-request received");
|
||||||
|
};
|
||||||
|
let id1 = req1.id.clone();
|
||||||
|
// When we answer a message, we get the same ID
|
||||||
|
if let Message::Response(ref resp) = req1.reply(json!([1, 2, 3])) {
|
||||||
|
assert_eq!(
|
||||||
|
*resp,
|
||||||
|
Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Ok(json!([1, 2, 3])),
|
||||||
|
id: id1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Not a response");
|
||||||
|
}
|
||||||
|
let id2 = req2.id.clone();
|
||||||
|
// The same with an error
|
||||||
|
if let Message::Response(ref resp) =
|
||||||
|
req2.error(RpcError::new(42, "Wrong!".to_owned(), None))
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
*resp,
|
||||||
|
Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Err(RpcError::new(42, "Wrong!".to_owned(), None)),
|
||||||
|
id: id2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Not a response");
|
||||||
|
}
|
||||||
|
// When we have unmatched, we generate a top-level error with Null id.
|
||||||
|
if let Message::Response(ref resp) =
|
||||||
|
Message::error(RpcError::new(43, "Also wrong!".to_owned(), None))
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
*resp,
|
||||||
|
Response {
|
||||||
|
jsonrpc: Version,
|
||||||
|
result: Err(RpcError::new(43, "Also wrong!".to_owned(), None)),
|
||||||
|
id: Value::Null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Not a response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
rpc_primitives/src/parser.rs
Normal file
27
rpc_primitives/src/parser.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::errors::RpcParseError;
|
||||||
|
|
||||||
|
pub trait RpcRequest: Sized {
|
||||||
|
fn parse(value: Option<Value>) -> Result<Self, RpcParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_params<T: DeserializeOwned>(value: Option<Value>) -> Result<T, RpcParseError> {
|
||||||
|
if let Some(value) = value {
|
||||||
|
serde_json::from_value(value)
|
||||||
|
.map_err(|err| RpcParseError(format!("Failed parsing args: {err}")))
|
||||||
|
} else {
|
||||||
|
Err(RpcParseError("Require at least one parameter".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! parse_request {
|
||||||
|
($request_name:ty) => {
|
||||||
|
impl RpcRequest for $request_name {
|
||||||
|
fn parse(value: Option<Value>) -> Result<Self, RpcParseError> {
|
||||||
|
parse_params::<Self>(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -9,6 +9,9 @@ serde_json.workspace = true
|
|||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
actix.workspace = true
|
||||||
|
actix-cors.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
|
|
||||||
@ -22,4 +25,7 @@ path = "../consensus"
|
|||||||
path = "../networking"
|
path = "../networking"
|
||||||
|
|
||||||
[dependencies.sequencer_core]
|
[dependencies.sequencer_core]
|
||||||
path = "../sequencer_core"
|
path = "../sequencer_core"
|
||||||
|
|
||||||
|
[dependencies.rpc_primitives]
|
||||||
|
path = "../rpc_primitives"
|
||||||
@ -1 +1,39 @@
|
|||||||
//ToDo: Add sequencer_rpc module
|
pub mod net_utils;
|
||||||
|
pub mod process;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use rpc_primitives::{
|
||||||
|
errors::{RpcError, RpcErrorKind},
|
||||||
|
RpcPollingConfig,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub use net_utils::*;
|
||||||
|
|
||||||
|
use self::types::err_rpc::RpcErr;
|
||||||
|
|
||||||
|
//ToDo: Add necessary fields
|
||||||
|
pub struct JsonHandler {
|
||||||
|
pub polling_config: RpcPollingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn respond<T: Serialize>(val: T) -> Result<Value, RpcErr> {
|
||||||
|
Ok(serde_json::to_value(val)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError {
|
||||||
|
let mut content: Option<Value> = None;
|
||||||
|
if err.error_struct.is_some() {
|
||||||
|
content = match err.error_struct.clone().unwrap() {
|
||||||
|
RpcErrorKind::HandlerError(val) | RpcErrorKind::InternalError(val) => Some(val),
|
||||||
|
RpcErrorKind::RequestValidationError(vall) => Some(serde_json::to_value(vall).unwrap()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
RpcError {
|
||||||
|
error_struct: None,
|
||||||
|
code: err.code,
|
||||||
|
message: err.message,
|
||||||
|
data: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
64
sequencer_rpc/src/net_utils.rs
Normal file
64
sequencer_rpc/src/net_utils.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{http, middleware, web, App, Error as HttpError, HttpResponse, HttpServer};
|
||||||
|
use futures::Future;
|
||||||
|
use futures::FutureExt;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use rpc_primitives::message::Message;
|
||||||
|
use rpc_primitives::RpcConfig;
|
||||||
|
|
||||||
|
use super::JsonHandler;
|
||||||
|
|
||||||
|
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
|
fn rpc_handler(
|
||||||
|
message: web::Json<Message>,
|
||||||
|
handler: web::Data<JsonHandler>,
|
||||||
|
) -> impl Future<Output = Result<HttpResponse, HttpError>> {
|
||||||
|
let response = async move {
|
||||||
|
let message = handler.process(message.0).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(&message))
|
||||||
|
};
|
||||||
|
response.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cors(cors_allowed_origins: &[String]) -> Cors {
|
||||||
|
let mut cors = Cors::permissive();
|
||||||
|
if cors_allowed_origins != ["*".to_string()] {
|
||||||
|
for origin in cors_allowed_origins {
|
||||||
|
cors = cors.allowed_origin(origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cors.allowed_methods(vec!["GET", "POST"])
|
||||||
|
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||||
|
.allowed_header(http::header::CONTENT_TYPE)
|
||||||
|
.max_age(3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new_http_server(config: RpcConfig) -> io::Result<actix_web::dev::Server> {
|
||||||
|
let RpcConfig {
|
||||||
|
addr,
|
||||||
|
cors_allowed_origins,
|
||||||
|
polling_config,
|
||||||
|
limits_config,
|
||||||
|
} = config;
|
||||||
|
info!(target:"network", "Starting http server at {}", addr);
|
||||||
|
let handler = web::Data::new(JsonHandler { polling_config });
|
||||||
|
|
||||||
|
// HTTP server
|
||||||
|
Ok(HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(get_cors(&cors_allowed_origins))
|
||||||
|
.app_data(handler.clone())
|
||||||
|
.app_data(web::JsonConfig::default().limit(limits_config.json_payload_max_size))
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
.service(web::resource("/").route(web::post().to(rpc_handler)))
|
||||||
|
})
|
||||||
|
.bind(addr)?
|
||||||
|
.shutdown_timeout(SHUTDOWN_TIMEOUT_SECS)
|
||||||
|
.disable_signals()
|
||||||
|
.run())
|
||||||
|
}
|
||||||
53
sequencer_rpc/src/process.rs
Normal file
53
sequencer_rpc/src/process.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use actix_web::Error as HttpError;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use rpc_primitives::{
|
||||||
|
errors::RpcError,
|
||||||
|
message::{Message, Request},
|
||||||
|
parser::RpcRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
rpc_error_responce_inverter,
|
||||||
|
types::rpc_structs::{HelloRequest, HelloResponse},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{respond, types::err_rpc::RpcErr, JsonHandler};
|
||||||
|
|
||||||
|
impl JsonHandler {
|
||||||
|
pub async fn process(&self, message: Message) -> Result<Message, HttpError> {
|
||||||
|
let id = message.id();
|
||||||
|
if let Message::Request(request) = message {
|
||||||
|
let message_inner = self
|
||||||
|
.process_request_internal(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.0)
|
||||||
|
.map_err(rpc_error_responce_inverter);
|
||||||
|
Ok(Message::response(id, message_inner))
|
||||||
|
} else {
|
||||||
|
Ok(Message::error(RpcError::parse_error(
|
||||||
|
"JSON RPC Request format was expected".to_owned(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
///Example of request processing
|
||||||
|
async fn process_temp_hello(&self, request: Request) -> Result<Value, RpcErr> {
|
||||||
|
let _hello_request = HelloRequest::parse(Some(request.params))?;
|
||||||
|
|
||||||
|
let helperstruct = HelloResponse {
|
||||||
|
greeting: "HELLO_FROM_SEQUENCER".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
respond(helperstruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_request_internal(&self, request: Request) -> Result<Value, RpcErr> {
|
||||||
|
match request.method.as_ref() {
|
||||||
|
//Todo : Add handling of more JSON RPC methods
|
||||||
|
"hello" => self.process_temp_hello(request).await,
|
||||||
|
_ => Err(RpcErr(RpcError::method_not_found(request.method))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
sequencer_rpc/src/types/err_rpc.rs
Normal file
47
sequencer_rpc/src/types/err_rpc.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use rpc_primitives::errors::{RpcError, RpcParseError};
|
||||||
|
|
||||||
|
pub struct RpcErr(pub RpcError);
|
||||||
|
|
||||||
|
pub type RpcErrInternal = anyhow::Error;
|
||||||
|
|
||||||
|
pub trait RpcErrKind: 'static {
|
||||||
|
fn into_rpc_err(self) -> RpcError;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: RpcErrKind> From<T> for RpcErr {
|
||||||
|
fn from(e: T) -> Self {
|
||||||
|
Self(e.into_rpc_err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! standard_rpc_err_kind {
|
||||||
|
($type_name:path) => {
|
||||||
|
impl RpcErrKind for $type_name {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
standard_rpc_err_kind!(RpcError);
|
||||||
|
standard_rpc_err_kind!(RpcParseError);
|
||||||
|
|
||||||
|
impl RpcErrKind for serde_json::Error {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
RpcError::serialization_error(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcErrKind for RpcErrInternal {
|
||||||
|
fn into_rpc_err(self) -> RpcError {
|
||||||
|
RpcError::new_internal_error(None, &format!("{self:#?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn from_rpc_err_into_anyhow_err(rpc_err: RpcError) -> anyhow::Error {
|
||||||
|
debug!("Rpc error cast to anyhow error : err {rpc_err:?}");
|
||||||
|
anyhow::anyhow!(format!("{rpc_err:#?}"))
|
||||||
|
}
|
||||||
3
sequencer_rpc/src/types/mod.rs
Normal file
3
sequencer_rpc/src/types/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod err_rpc;
|
||||||
|
pub mod parse;
|
||||||
|
pub mod rpc_structs;
|
||||||
1
sequencer_rpc/src/types/parse.rs
Normal file
1
sequencer_rpc/src/types/parse.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
16
sequencer_rpc/src/types/rpc_structs.rs
Normal file
16
sequencer_rpc/src/types/rpc_structs.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use rpc_primitives::errors::RpcParseError;
|
||||||
|
use rpc_primitives::parse_request;
|
||||||
|
use rpc_primitives::parser::parse_params;
|
||||||
|
use rpc_primitives::parser::RpcRequest;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct HelloRequest {}
|
||||||
|
|
||||||
|
parse_request!(HelloRequest);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct HelloResponse {
|
||||||
|
pub greeting: String,
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ serde_json.workspace = true
|
|||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
actix.workspace = true
|
||||||
|
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@ -26,4 +27,7 @@ path = "../networking"
|
|||||||
path = "../sequencer_rpc"
|
path = "../sequencer_rpc"
|
||||||
|
|
||||||
[dependencies.sequencer_core]
|
[dependencies.sequencer_core]
|
||||||
path = "../sequencer_core"
|
path = "../sequencer_core"
|
||||||
|
|
||||||
|
[dependencies.rpc_primitives]
|
||||||
|
path = "../rpc_primitives"
|
||||||
17
sequencer_runner/src/lib.rs
Normal file
17
sequencer_runner/src/lib.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
use rpc_primitives::RpcConfig;
|
||||||
|
use sequencer_rpc::new_http_server;
|
||||||
|
|
||||||
|
pub async fn main_runner() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let http_server = new_http_server(RpcConfig::default())?;
|
||||||
|
info!("HTTP server started");
|
||||||
|
let _http_server_handle = http_server.handle();
|
||||||
|
tokio::spawn(http_server);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
//ToDo: Insert activity into main loop
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,16 @@
|
|||||||
//ToDo: Add sequencer_runner module
|
use anyhow::Result;
|
||||||
|
|
||||||
fn main() {
|
use sequencer_runner::main_runner;
|
||||||
println!("Hello, world!");
|
|
||||||
|
pub const NUM_THREADS: usize = 4;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
actix::System::with_tokio_rt(|| {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(NUM_THREADS)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.block_on(main_runner())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add storage module
|
//ToDo: Add storage module
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add utxo module
|
//ToDo: Add utxo module
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
//ToDo: Add zkvm module
|
//ToDo: Add zkvm module
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user