1pub mod jsonrpc_types;
20
21mod parser;
22mod util;
23
24use crate::lotus_json::HasLotusJson;
25
26use self::{jsonrpc_types::RequestParameters, util::Optional as _};
27use super::error::ServerError as Error;
28use ahash::HashMap;
29use anyhow::Context as _;
30use enumflags2::{BitFlags, bitflags, make_bitflags};
31use fvm_ipld_blockstore::Blockstore;
32use http::Uri;
33use jsonrpsee::RpcModule;
34use openrpc_types::{ContentDescriptor, Method, ParamStructure, ReferenceOr};
35use parser::Parser;
36use schemars::{JsonSchema, Schema, SchemaGenerator};
37use serde::{
38 Deserialize, Serialize,
39 de::{Error as _, Unexpected},
40};
41use std::{future::Future, str::FromStr, sync::Arc};
42use strum::EnumString;
43
44pub type Ctx<T> = Arc<crate::rpc::RPCState<T>>;
46
47pub trait RpcMethod<const ARITY: usize> {
59 const N_REQUIRED_PARAMS: usize = ARITY;
61 const NAME: &'static str;
63 const NAME_ALIAS: Option<&'static str> = None;
65 const PARAM_NAMES: [&'static str; ARITY];
67 const API_PATHS: BitFlags<ApiPaths>;
69 const PERMISSION: Permission;
71 const SUMMARY: Option<&'static str> = None;
73 const DESCRIPTION: Option<&'static str> = None;
75 type Params: Params<ARITY>;
77 type Ok: HasLotusJson;
79 fn handle(
81 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
82 params: Self::Params,
83 ) -> impl Future<Output = Result<Self::Ok, Error>> + Send;
84 const SUBSCRIPTION: bool = false;
86}
87
88#[derive(
90 Debug,
91 Clone,
92 Copy,
93 PartialEq,
94 Eq,
95 PartialOrd,
96 Ord,
97 Hash,
98 derive_more::Display,
99 Serialize,
100 Deserialize,
101)]
102#[serde(rename_all = "snake_case")]
103pub enum Permission {
104 Admin,
106 Sign,
108 Write,
110 Read,
112}
113
114#[bitflags]
118#[repr(u8)]
119#[derive(
120 Debug,
121 Default,
122 Clone,
123 Copy,
124 Hash,
125 Eq,
126 PartialEq,
127 Ord,
128 PartialOrd,
129 clap::ValueEnum,
130 EnumString,
131 Deserialize,
132 Serialize,
133)]
134pub enum ApiPaths {
135 #[strum(ascii_case_insensitive)]
137 V0 = 0b00000001,
138 #[strum(ascii_case_insensitive)]
140 #[default]
141 V1 = 0b00000010,
142 #[strum(ascii_case_insensitive)]
144 V2 = 0b00000100,
145}
146
147impl ApiPaths {
148 pub const fn all() -> BitFlags<Self> {
149 make_bitflags!(Self::{ V0 | V1 })
151 }
152
153 pub const fn all_with_v2() -> BitFlags<Self> {
157 Self::all().union_c(make_bitflags!(Self::{ V2 }))
158 }
159
160 pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
161 Ok(Self::from_str(uri.path().trim_start_matches("/rpc/"))?)
162 }
163
164 pub fn path(&self) -> &'static str {
165 match self {
166 Self::V0 => "rpc/v0",
167 Self::V1 => "rpc/v1",
168 Self::V2 => "rpc/v2",
169 }
170 }
171}
172
173pub trait RpcMethodExt<const ARITY: usize>: RpcMethod<ARITY> {
176 fn build_params(
180 params: Self::Params,
181 calling_convention: ConcreteCallingConvention,
182 ) -> Result<RequestParameters, serde_json::Error> {
183 let args = params.unparse()?;
184 match calling_convention {
185 ConcreteCallingConvention::ByPosition => {
186 Ok(RequestParameters::ByPosition(Vec::from(args)))
187 }
188 ConcreteCallingConvention::ByName => Ok(RequestParameters::ByName(
189 itertools::zip_eq(Self::PARAM_NAMES.into_iter().map(String::from), args).collect(),
190 )),
191 }
192 }
193
194 fn parse_params(
195 params_raw: Option<impl AsRef<str>>,
196 calling_convention: ParamStructure,
197 ) -> anyhow::Result<Self::Params> {
198 Ok(Self::Params::parse(
199 params_raw
200 .map(|s| serde_json::from_str(s.as_ref()))
201 .transpose()?,
202 Self::PARAM_NAMES,
203 calling_convention,
204 Self::N_REQUIRED_PARAMS,
205 )?)
206 }
207
208 fn openrpc<'de>(
210 g: &mut SchemaGenerator,
211 calling_convention: ParamStructure,
212 method_name: &'static str,
213 ) -> Method
214 where
215 <Self::Ok as HasLotusJson>::LotusJson: JsonSchema + Deserialize<'de>,
216 {
217 Method {
218 name: String::from(method_name),
219 params: itertools::zip_eq(Self::PARAM_NAMES, Self::Params::schemas(g))
220 .enumerate()
221 .map(|(pos, (name, (schema, nullable)))| {
222 let required = pos <= Self::N_REQUIRED_PARAMS;
223 if !required && !nullable {
224 panic!("Optional parameter at position {pos} should be of an optional type. method={method_name}, param_name={name}");
225 }
226 ReferenceOr::Item(ContentDescriptor {
227 name: String::from(name),
228 schema,
229 required: Some(required),
230 ..Default::default()
231 })
232 })
233 .collect(),
234 param_structure: Some(calling_convention),
235 result: Some(ReferenceOr::Item(ContentDescriptor {
236 name: format!("{method_name}.Result"),
237 schema: g.subschema_for::<<Self::Ok as HasLotusJson>::LotusJson>(),
238 required: Some(!<Self::Ok as HasLotusJson>::LotusJson::optional()),
239 ..Default::default()
240 })),
241 summary: Self::SUMMARY.map(Into::into),
242 description: Self::DESCRIPTION.map(Into::into),
243 ..Default::default()
244 }
245 }
246
247 fn register(
249 modules: &mut HashMap<
250 ApiPaths,
251 RpcModule<crate::rpc::RPCState<impl Blockstore + Send + Sync + 'static>>,
252 >,
253 calling_convention: ParamStructure,
254 ) -> Result<(), jsonrpsee::core::RegisterMethodError>
255 where
256 <Self::Ok as HasLotusJson>::LotusJson: Clone + 'static,
257 {
258 use clap::ValueEnum as _;
259
260 assert!(
261 Self::N_REQUIRED_PARAMS <= ARITY,
262 "N_REQUIRED_PARAMS({}) can not be greater than ARITY({ARITY}) in {}",
263 Self::N_REQUIRED_PARAMS,
264 Self::NAME
265 );
266
267 for api_version in ApiPaths::value_variants() {
268 if Self::API_PATHS.contains(*api_version)
269 && let Some(module) = modules.get_mut(api_version)
270 {
271 module.register_async_method(
272 Self::NAME,
273 move |params, ctx, _extensions| async move {
274 let params = Self::parse_params(params.as_str(), calling_convention)
275 .map_err(|e| Error::invalid_params(e, None))?;
276 let ok = Self::handle(ctx, params).await?;
277 Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json())
278 },
279 )?;
280 if let Some(alias) = Self::NAME_ALIAS {
281 module.register_alias(alias, Self::NAME)?
282 }
283 }
284 }
285 Ok(())
286 }
287 fn request(params: Self::Params) -> Result<crate::rpc::Request<Self::Ok>, serde_json::Error> {
289 let params = Self::request_params(params)?;
291 Ok(crate::rpc::Request {
292 method_name: Self::NAME.into(),
293 params,
294 result_type: std::marker::PhantomData,
295 api_paths: Self::API_PATHS,
296 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
297 })
298 }
299
300 fn request_params(params: Self::Params) -> Result<serde_json::Value, serde_json::Error> {
301 Ok(
303 match Self::build_params(params, ConcreteCallingConvention::ByPosition)? {
304 RequestParameters::ByPosition(mut it) => {
305 while Self::N_REQUIRED_PARAMS < it.len() {
309 match it.last() {
310 Some(last) if last.is_null() => it.pop(),
311 _ => break,
312 };
313 }
314 serde_json::Value::Array(it)
315 }
316 RequestParameters::ByName(it) => serde_json::Value::Object(it),
317 },
318 )
319 }
320
321 fn request_with_alias(
323 params: Self::Params,
324 use_alias: bool,
325 ) -> anyhow::Result<crate::rpc::Request<Self::Ok>> {
326 let params = Self::request_params(params)?;
327 let name = if use_alias {
328 Self::NAME_ALIAS.context("alias is None")?
329 } else {
330 Self::NAME
331 };
332
333 Ok(crate::rpc::Request {
334 method_name: name.into(),
335 params,
336 result_type: std::marker::PhantomData,
337 api_paths: Self::API_PATHS,
338 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
339 })
340 }
341 fn call_raw(
342 client: &crate::rpc::client::Client,
343 params: Self::Params,
344 ) -> impl Future<Output = Result<<Self::Ok as HasLotusJson>::LotusJson, jsonrpsee::core::ClientError>>
345 {
346 async {
347 let json = client.call(Self::request(params)?.map_ty()).await?;
351 Ok(serde_json::from_value(json)?)
352 }
353 }
354 fn call(
355 client: &crate::rpc::client::Client,
356 params: Self::Params,
357 ) -> impl Future<Output = Result<Self::Ok, jsonrpsee::core::ClientError>> {
358 async {
359 Self::call_raw(client, params)
360 .await
361 .map(Self::Ok::from_lotus_json)
362 }
363 }
364}
365impl<const ARITY: usize, T> RpcMethodExt<ARITY> for T where T: RpcMethod<ARITY> {}
366
367pub trait Params<const ARITY: usize>: HasLotusJson {
371 fn schemas(g: &mut SchemaGenerator) -> [(Schema, bool); ARITY];
374 fn parse(
377 raw: Option<RequestParameters>,
378 names: [&str; ARITY],
379 calling_convention: ParamStructure,
380 n_required: usize,
381 ) -> Result<Self, Error>
382 where
383 Self: Sized;
384 fn unparse(self) -> Result<[serde_json::Value; ARITY], serde_json::Error> {
388 match self.into_lotus_json_value() {
389 Ok(serde_json::Value::Array(args)) => match args.try_into() {
390 Ok(it) => Ok(it),
391 Err(_) => Err(serde_json::Error::custom("ARITY mismatch")),
392 },
393 Ok(serde_json::Value::Null) if ARITY == 0 => {
394 Ok(std::array::from_fn(|_ix| Default::default()))
395 }
396 Ok(it) => Err(serde_json::Error::invalid_type(
397 unexpected(&it),
398 &"a Vec with an item for each argument",
399 )),
400 Err(e) => Err(e),
401 }
402 }
403}
404
405fn unexpected(v: &serde_json::Value) -> Unexpected<'_> {
406 match v {
407 serde_json::Value::Null => Unexpected::Unit,
408 serde_json::Value::Bool(it) => Unexpected::Bool(*it),
409 serde_json::Value::Number(it) => match (it.as_f64(), it.as_i64(), it.as_u64()) {
410 (None, None, None) => Unexpected::Other("Number"),
411 (Some(it), _, _) => Unexpected::Float(it),
412 (_, Some(it), _) => Unexpected::Signed(it),
413 (_, _, Some(it)) => Unexpected::Unsigned(it),
414 },
415 serde_json::Value::String(it) => Unexpected::Str(it),
416 serde_json::Value::Array(_) => Unexpected::Seq,
417 serde_json::Value::Object(_) => Unexpected::Map,
418 }
419}
420
421macro_rules! do_impls {
422 ($arity:literal $(, $arg:ident)* $(,)?) => {
423 const _: () = {
424 let _assert: [&str; $arity] = [$(stringify!($arg)),*];
425 };
426
427 impl<$($arg),*> Params<$arity> for ($($arg,)*)
428 where
429 $($arg: HasLotusJson + Clone, <$arg as HasLotusJson>::LotusJson: JsonSchema, )*
430 {
431 fn parse(
432 raw: Option<RequestParameters>,
433 arg_names: [&str; $arity],
434 calling_convention: ParamStructure,
435 n_required: usize,
436 ) -> Result<Self, Error> {
437 let mut _parser = Parser::new(raw, &arg_names, calling_convention, n_required)?;
438 Ok(($(_parser.parse::<crate::lotus_json::LotusJson<$arg>>()?.into_inner(),)*))
439 }
440 fn schemas(_gen: &mut SchemaGenerator) -> [(Schema, bool); $arity] {
441 [$((_gen.subschema_for::<$arg::LotusJson>(), $arg::LotusJson::optional())),*]
442 }
443 }
444 };
445}
446
447do_impls!(0);
448do_impls!(1, T0);
449do_impls!(2, T0, T1);
450do_impls!(3, T0, T1, T2);
451do_impls!(4, T0, T1, T2, T3);
452pub enum ConcreteCallingConvention {
462 ByPosition,
463 #[allow(unused)] ByName,
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_api_paths_from_uri() {
473 let v0 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v0".parse().unwrap()).unwrap();
474 assert_eq!(v0, ApiPaths::V0);
475 let v1 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v1".parse().unwrap()).unwrap();
476 assert_eq!(v1, ApiPaths::V1);
477 let v2 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v2".parse().unwrap()).unwrap();
478 assert_eq!(v2, ApiPaths::V2);
479
480 ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v3".parse().unwrap()).unwrap_err();
481 }
482}