pub mod jsonrpc_types;
mod parser;
mod util;
use crate::lotus_json::HasLotusJson;
use self::{jsonrpc_types::RequestParameters, util::Optional as _};
use super::error::ServerError as Error;
use ahash::HashMap;
use anyhow::Context as _;
use enumflags2::{BitFlags, bitflags, make_bitflags};
use fvm_ipld_blockstore::Blockstore;
use http::{Extensions, Uri};
use jsonrpsee::RpcModule;
use openrpc_types::{ContentDescriptor, Method, ParamStructure, ReferenceOr};
use parser::Parser;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::{
Deserialize, Serialize,
de::{Error as _, Unexpected},
};
use std::{future::Future, str::FromStr, sync::Arc};
use strum::EnumString;
pub type Ctx<T> = Arc<crate::rpc::RPCState<T>>;
pub trait RpcMethod<const ARITY: usize> {
const N_REQUIRED_PARAMS: usize = ARITY;
const NAME: &'static str;
const NAME_ALIAS: Option<&'static str> = None;
const PARAM_NAMES: [&'static str; ARITY];
const API_PATHS: BitFlags<ApiPaths>;
const PERMISSION: Permission;
const SUMMARY: Option<&'static str> = None;
const DESCRIPTION: Option<&'static str> = None;
type Params: Params<ARITY>;
type Ok: HasLotusJson;
fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
params: Self::Params,
ext: &Extensions,
) -> impl Future<Output = Result<Self::Ok, Error>> + Send;
const SUBSCRIPTION: bool = false;
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
derive_more::Display,
Serialize,
Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Permission {
Admin,
Sign,
Write,
Read,
}
#[bitflags]
#[repr(u8)]
#[derive(
Debug,
Default,
Clone,
Copy,
Hash,
Eq,
PartialEq,
Ord,
PartialOrd,
clap::ValueEnum,
EnumString,
Deserialize,
Serialize,
)]
pub enum ApiPaths {
#[strum(ascii_case_insensitive)]
V0 = 0b00000001,
#[strum(ascii_case_insensitive)]
#[default]
V1 = 0b00000010,
#[strum(ascii_case_insensitive)]
V2 = 0b00000100,
}
impl ApiPaths {
pub const fn all() -> BitFlags<Self> {
make_bitflags!(Self::{ V0 | V1 })
}
pub const fn all_with_v2() -> BitFlags<Self> {
Self::all().union_c(make_bitflags!(Self::{ V2 }))
}
pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
Ok(Self::from_str(uri.path().trim_start_matches("/rpc/"))?)
}
pub fn path(&self) -> &'static str {
match self {
Self::V0 => "rpc/v0",
Self::V1 => "rpc/v1",
Self::V2 => "rpc/v2",
}
}
}
pub trait RpcMethodExt<const ARITY: usize>: RpcMethod<ARITY> {
fn build_params(
params: Self::Params,
calling_convention: ConcreteCallingConvention,
) -> Result<RequestParameters, serde_json::Error> {
let args = params.unparse()?;
match calling_convention {
ConcreteCallingConvention::ByPosition => {
Ok(RequestParameters::ByPosition(Vec::from(args)))
}
ConcreteCallingConvention::ByName => Ok(RequestParameters::ByName(
itertools::zip_eq(Self::PARAM_NAMES.into_iter().map(String::from), args).collect(),
)),
}
}
fn parse_params(
params_raw: Option<impl AsRef<str>>,
calling_convention: ParamStructure,
) -> anyhow::Result<Self::Params> {
Ok(Self::Params::parse(
params_raw
.map(|s| serde_json::from_str(s.as_ref()))
.transpose()?,
Self::PARAM_NAMES,
calling_convention,
Self::N_REQUIRED_PARAMS,
)?)
}
fn openrpc<'de>(
g: &mut SchemaGenerator,
calling_convention: ParamStructure,
method_name: &'static str,
) -> Method
where
<Self::Ok as HasLotusJson>::LotusJson: JsonSchema + Deserialize<'de>,
{
Method {
name: String::from(method_name),
params: itertools::zip_eq(Self::PARAM_NAMES, Self::Params::schemas(g))
.enumerate()
.map(|(pos, (name, (schema, nullable)))| {
let required = pos <= Self::N_REQUIRED_PARAMS;
if !required && !nullable {
panic!("Optional parameter at position {pos} should be of an optional type. method={method_name}, param_name={name}");
}
ReferenceOr::Item(ContentDescriptor {
name: String::from(name),
schema,
required: Some(required),
..Default::default()
})
})
.collect(),
param_structure: Some(calling_convention),
result: Some(ReferenceOr::Item(ContentDescriptor {
name: format!("{method_name}.Result"),
schema: g.subschema_for::<<Self::Ok as HasLotusJson>::LotusJson>(),
required: Some(!<Self::Ok as HasLotusJson>::LotusJson::optional()),
..Default::default()
})),
summary: Self::SUMMARY.map(Into::into),
description: Self::DESCRIPTION.map(Into::into),
..Default::default()
}
}
fn register(
modules: &mut HashMap<
ApiPaths,
RpcModule<crate::rpc::RPCState<impl Blockstore + Send + Sync + 'static>>,
>,
calling_convention: ParamStructure,
) -> Result<(), jsonrpsee::core::RegisterMethodError>
where
<Self::Ok as HasLotusJson>::LotusJson: Clone + 'static,
{
use clap::ValueEnum as _;
assert!(
Self::N_REQUIRED_PARAMS <= ARITY,
"N_REQUIRED_PARAMS({}) can not be greater than ARITY({ARITY}) in {}",
Self::N_REQUIRED_PARAMS,
Self::NAME
);
for api_version in ApiPaths::value_variants() {
if Self::API_PATHS.contains(*api_version)
&& let Some(module) = modules.get_mut(api_version)
{
module.register_async_method(
Self::NAME,
move |params, ctx, extensions| async move {
let params = Self::parse_params(params.as_str(), calling_convention)
.map_err(|e| Error::invalid_params(e, None))?;
let ok = Self::handle(ctx, params, &extensions).await?;
Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json())
},
)?;
if let Some(alias) = Self::NAME_ALIAS {
module.register_alias(alias, Self::NAME)?
}
}
}
Ok(())
}
fn request(params: Self::Params) -> serde_json::Result<crate::rpc::Request<Self::Ok>> {
let params = Self::request_params(params)?;
Ok(crate::rpc::Request {
method_name: Self::NAME.into(),
params,
result_type: std::marker::PhantomData,
api_path: crate::rpc::Request::<Self::Ok>::max_api_path(Self::API_PATHS)
.map_err(serde_json::Error::custom)?,
timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
})
}
fn request_params(params: Self::Params) -> serde_json::Result<serde_json::Value> {
Ok(
match Self::build_params(params, ConcreteCallingConvention::ByPosition)? {
RequestParameters::ByPosition(mut it) => {
while Self::N_REQUIRED_PARAMS < it.len() {
match it.last() {
Some(last) if last.is_null() => it.pop(),
_ => break,
};
}
serde_json::Value::Array(it)
}
RequestParameters::ByName(it) => serde_json::Value::Object(it),
},
)
}
fn request_with_alias(
params: Self::Params,
use_alias: bool,
) -> anyhow::Result<crate::rpc::Request<Self::Ok>> {
let params = Self::request_params(params)?;
let name = if use_alias {
Self::NAME_ALIAS.context("alias is None")?
} else {
Self::NAME
};
Ok(crate::rpc::Request {
method_name: name.into(),
params,
result_type: std::marker::PhantomData,
api_path: crate::rpc::Request::<Self::Ok>::max_api_path(Self::API_PATHS)?,
timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
})
}
fn call_raw(
client: &crate::rpc::client::Client,
params: Self::Params,
) -> impl Future<Output = Result<<Self::Ok as HasLotusJson>::LotusJson, jsonrpsee::core::ClientError>>
{
async {
let json = client.call(Self::request(params)?.map_ty()).await?;
Ok(crate::rpc::json_validator::from_value_rejecting_unknown_fields(json)?)
}
}
fn call(
client: &crate::rpc::client::Client,
params: Self::Params,
) -> impl Future<Output = Result<Self::Ok, jsonrpsee::core::ClientError>> {
async {
Self::call_raw(client, params)
.await
.map(Self::Ok::from_lotus_json)
}
}
fn api_path(ext: &http::Extensions) -> anyhow::Result<ApiPaths> {
ext.get::<ApiPaths>()
.copied()
.context("failed to resolve api path")
}
}
impl<const ARITY: usize, T> RpcMethodExt<ARITY> for T where T: RpcMethod<ARITY> {}
pub trait Params<const ARITY: usize>: HasLotusJson {
fn schemas(g: &mut SchemaGenerator) -> [(Schema, bool); ARITY];
fn parse(
raw: Option<RequestParameters>,
names: [&str; ARITY],
calling_convention: ParamStructure,
n_required: usize,
) -> Result<Self, Error>
where
Self: Sized;
fn unparse(self) -> Result<[serde_json::Value; ARITY], serde_json::Error> {
match self.into_lotus_json_value() {
Ok(serde_json::Value::Array(args)) => match args.try_into() {
Ok(it) => Ok(it),
Err(_) => Err(serde_json::Error::custom("ARITY mismatch")),
},
Ok(serde_json::Value::Null) if ARITY == 0 => {
Ok(std::array::from_fn(|_ix| Default::default()))
}
Ok(it) => Err(serde_json::Error::invalid_type(
unexpected(&it),
&"a Vec with an item for each argument",
)),
Err(e) => Err(e),
}
}
}
fn unexpected(v: &serde_json::Value) -> Unexpected<'_> {
match v {
serde_json::Value::Null => Unexpected::Unit,
serde_json::Value::Bool(it) => Unexpected::Bool(*it),
serde_json::Value::Number(it) => match (it.as_f64(), it.as_i64(), it.as_u64()) {
(None, None, None) => Unexpected::Other("Number"),
(Some(it), _, _) => Unexpected::Float(it),
(_, Some(it), _) => Unexpected::Signed(it),
(_, _, Some(it)) => Unexpected::Unsigned(it),
},
serde_json::Value::String(it) => Unexpected::Str(it),
serde_json::Value::Array(_) => Unexpected::Seq,
serde_json::Value::Object(_) => Unexpected::Map,
}
}
macro_rules! do_impls {
($arity:literal $(, $arg:ident)* $(,)?) => {
const _: () = {
let _assert: [&str; $arity] = [$(stringify!($arg)),*];
};
impl<$($arg),*> Params<$arity> for ($($arg,)*)
where
$($arg: HasLotusJson + Clone, <$arg as HasLotusJson>::LotusJson: JsonSchema, )*
{
fn parse(
raw: Option<RequestParameters>,
arg_names: [&str; $arity],
calling_convention: ParamStructure,
n_required: usize,
) -> Result<Self, Error> {
let mut _parser = Parser::new(raw, &arg_names, calling_convention, n_required)?;
Ok(($(_parser.parse::<crate::lotus_json::LotusJson<$arg>>()?.into_inner(),)*))
}
fn schemas(_gen: &mut SchemaGenerator) -> [(Schema, bool); $arity] {
[$((_gen.subschema_for::<$arg::LotusJson>(), $arg::LotusJson::optional())),*]
}
}
};
}
do_impls!(0);
do_impls!(1, T0);
do_impls!(2, T0, T1);
do_impls!(3, T0, T1, T2);
do_impls!(4, T0, T1, T2, T3);
pub enum ConcreteCallingConvention {
ByPosition,
#[allow(unused)] ByName,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_paths_from_uri() {
let v0 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v0".parse().unwrap()).unwrap();
assert_eq!(v0, ApiPaths::V0);
let v1 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v1".parse().unwrap()).unwrap();
assert_eq!(v1, ApiPaths::V1);
let v2 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v2".parse().unwrap()).unwrap();
assert_eq!(v2, ApiPaths::V2);
ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v3".parse().unwrap()).unwrap_err();
}
}