1pub mod jsonrpc_types;
20
21mod parser;
22mod util;
23
24use crate::{db::EthMappingsStore, 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::{Extensions, 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 + EthMappingsStore + Send + Sync + 'static>,
82 params: Self::Params,
83 ext: &Extensions,
84 ) -> impl Future<Output = Result<Self::Ok, Error>> + Send;
85 const SUBSCRIPTION: bool = false;
87}
88
89#[derive(
91 Debug,
92 Clone,
93 Copy,
94 PartialEq,
95 Eq,
96 PartialOrd,
97 Ord,
98 Hash,
99 derive_more::Display,
100 Serialize,
101 Deserialize,
102)]
103#[serde(rename_all = "snake_case")]
104pub enum Permission {
105 Admin,
107 Sign,
109 Write,
111 Read,
113}
114
115#[bitflags]
119#[repr(u8)]
120#[derive(
121 Debug,
122 Default,
123 Clone,
124 Copy,
125 Hash,
126 Eq,
127 PartialEq,
128 Ord,
129 PartialOrd,
130 clap::ValueEnum,
131 EnumString,
132 Deserialize,
133 Serialize,
134)]
135pub enum ApiPaths {
136 #[strum(ascii_case_insensitive)]
138 V0 = 0b00000001,
139 #[strum(ascii_case_insensitive)]
141 #[default]
142 V1 = 0b00000010,
143 #[strum(ascii_case_insensitive)]
145 V2 = 0b00000100,
146}
147
148impl ApiPaths {
149 pub const fn all() -> BitFlags<Self> {
150 make_bitflags!(Self::{ V0 | V1 })
152 }
153
154 pub const fn all_with_v2() -> BitFlags<Self> {
158 Self::all().union_c(make_bitflags!(Self::{ V2 }))
159 }
160
161 pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
162 Ok(Self::from_str(uri.path().trim_start_matches("/rpc/"))?)
163 }
164
165 pub fn path(&self) -> &'static str {
166 match self {
167 Self::V0 => "rpc/v0",
168 Self::V1 => "rpc/v1",
169 Self::V2 => "rpc/v2",
170 }
171 }
172}
173
174pub trait RpcMethodExt<const ARITY: usize>: RpcMethod<ARITY> {
177 fn build_params(
181 params: Self::Params,
182 calling_convention: ConcreteCallingConvention,
183 ) -> Result<RequestParameters, serde_json::Error> {
184 let args = params.unparse()?;
185 match calling_convention {
186 ConcreteCallingConvention::ByPosition => {
187 Ok(RequestParameters::ByPosition(Vec::from(args)))
188 }
189 ConcreteCallingConvention::ByName => Ok(RequestParameters::ByName(
190 itertools::zip_eq(Self::PARAM_NAMES.into_iter().map(String::from), args).collect(),
191 )),
192 }
193 }
194
195 fn parse_params(
196 params_raw: Option<impl AsRef<str>>,
197 calling_convention: ParamStructure,
198 ) -> anyhow::Result<Self::Params> {
199 Ok(Self::Params::parse(
200 params_raw
201 .map(|s| serde_json::from_str(s.as_ref()))
202 .transpose()?,
203 Self::PARAM_NAMES,
204 calling_convention,
205 Self::N_REQUIRED_PARAMS,
206 )?)
207 }
208
209 fn openrpc<'de>(
211 g: &mut SchemaGenerator,
212 calling_convention: ParamStructure,
213 method_name: &'static str,
214 ) -> Method
215 where
216 <Self::Ok as HasLotusJson>::LotusJson: JsonSchema + Deserialize<'de>,
217 {
218 Method {
219 name: String::from(method_name),
220 params: itertools::zip_eq(Self::PARAM_NAMES, Self::Params::schemas(g))
221 .enumerate()
222 .map(|(pos, (name, (schema, nullable)))| {
223 let required = pos <= Self::N_REQUIRED_PARAMS;
224 if !required && !nullable {
225 panic!("Optional parameter at position {pos} should be of an optional type. method={method_name}, param_name={name}");
226 }
227 ReferenceOr::Item(ContentDescriptor {
228 name: String::from(name),
229 schema,
230 required: Some(required),
231 ..Default::default()
232 })
233 })
234 .collect(),
235 param_structure: Some(calling_convention),
236 result: Some(ReferenceOr::Item(ContentDescriptor {
237 name: format!("{method_name}.Result"),
238 schema: g.subschema_for::<<Self::Ok as HasLotusJson>::LotusJson>(),
239 required: Some(!<Self::Ok as HasLotusJson>::LotusJson::optional()),
240 ..Default::default()
241 })),
242 summary: Self::SUMMARY.map(Into::into),
243 description: Self::DESCRIPTION.map(Into::into),
244 ..Default::default()
245 }
246 }
247
248 fn register(
250 modules: &mut HashMap<
251 ApiPaths,
252 RpcModule<
253 crate::rpc::RPCState<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
254 >,
255 >,
256 calling_convention: ParamStructure,
257 ) -> Result<(), jsonrpsee::core::RegisterMethodError>
258 where
259 <Self::Ok as HasLotusJson>::LotusJson: Clone + 'static,
260 {
261 use clap::ValueEnum as _;
262
263 assert!(
264 Self::N_REQUIRED_PARAMS <= ARITY,
265 "N_REQUIRED_PARAMS({}) can not be greater than ARITY({ARITY}) in {}",
266 Self::N_REQUIRED_PARAMS,
267 Self::NAME
268 );
269
270 for api_version in ApiPaths::value_variants() {
271 if Self::API_PATHS.contains(*api_version)
272 && let Some(module) = modules.get_mut(api_version)
273 {
274 module.register_async_method(
275 Self::NAME,
276 move |params, ctx, extensions| async move {
277 let params = Self::parse_params(params.as_str(), calling_convention)
278 .map_err(|e| Error::invalid_params(e, None))?;
279 let ok = Self::handle(ctx, params, &extensions).await?;
280 Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json())
281 },
282 )?;
283 if let Some(alias) = Self::NAME_ALIAS {
284 module.register_alias(alias, Self::NAME)?
285 }
286 }
287 }
288 Ok(())
289 }
290 fn request(params: Self::Params) -> serde_json::Result<crate::rpc::Request<Self::Ok>> {
292 let params = Self::request_params(params)?;
294 Ok(crate::rpc::Request {
295 method_name: Self::NAME.into(),
296 params,
297 result_type: std::marker::PhantomData,
298 api_path: crate::rpc::Request::<Self::Ok>::max_api_path(Self::API_PATHS)
299 .map_err(serde_json::Error::custom)?,
300 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
301 })
302 }
303
304 fn request_params(params: Self::Params) -> serde_json::Result<serde_json::Value> {
305 Ok(
307 match Self::build_params(params, ConcreteCallingConvention::ByPosition)? {
308 RequestParameters::ByPosition(mut it) => {
309 while Self::N_REQUIRED_PARAMS < it.len() {
313 match it.last() {
314 Some(last) if last.is_null() => it.pop(),
315 _ => break,
316 };
317 }
318 serde_json::Value::Array(it)
319 }
320 RequestParameters::ByName(it) => serde_json::Value::Object(it),
321 },
322 )
323 }
324
325 fn request_with_alias(
327 params: Self::Params,
328 use_alias: bool,
329 ) -> anyhow::Result<crate::rpc::Request<Self::Ok>> {
330 let params = Self::request_params(params)?;
331 let name = if use_alias {
332 Self::NAME_ALIAS.context("alias is None")?
333 } else {
334 Self::NAME
335 };
336
337 Ok(crate::rpc::Request {
338 method_name: name.into(),
339 params,
340 result_type: std::marker::PhantomData,
341 api_path: crate::rpc::Request::<Self::Ok>::max_api_path(Self::API_PATHS)?,
342 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
343 })
344 }
345 fn call_raw(
346 client: &crate::rpc::client::Client,
347 params: Self::Params,
348 ) -> impl Future<Output = Result<<Self::Ok as HasLotusJson>::LotusJson, jsonrpsee::core::ClientError>>
349 {
350 async {
351 let json = client.call(Self::request(params)?.map_ty()).await?;
355 Ok(crate::rpc::json_validator::from_value_rejecting_unknown_fields(json)?)
356 }
357 }
358 fn call(
359 client: &crate::rpc::client::Client,
360 params: Self::Params,
361 ) -> impl Future<Output = Result<Self::Ok, jsonrpsee::core::ClientError>> {
362 async {
363 Self::call_raw(client, params)
364 .await
365 .map(Self::Ok::from_lotus_json)
366 }
367 }
368
369 fn api_path(ext: &http::Extensions) -> anyhow::Result<ApiPaths> {
370 ext.get::<ApiPaths>()
371 .copied()
372 .context("failed to resolve api path")
373 }
374}
375impl<const ARITY: usize, T> RpcMethodExt<ARITY> for T where T: RpcMethod<ARITY> {}
376
377pub trait Params<const ARITY: usize>: HasLotusJson {
381 fn schemas(g: &mut SchemaGenerator) -> [(Schema, bool); ARITY];
384 fn parse(
387 raw: Option<RequestParameters>,
388 names: [&str; ARITY],
389 calling_convention: ParamStructure,
390 n_required: usize,
391 ) -> Result<Self, Error>
392 where
393 Self: Sized;
394 fn unparse(self) -> Result<[serde_json::Value; ARITY], serde_json::Error> {
398 match self.into_lotus_json_value() {
399 Ok(serde_json::Value::Array(args)) => match args.try_into() {
400 Ok(it) => Ok(it),
401 Err(_) => Err(serde_json::Error::custom("ARITY mismatch")),
402 },
403 Ok(serde_json::Value::Null) if ARITY == 0 => {
404 Ok(std::array::from_fn(|_ix| Default::default()))
405 }
406 Ok(it) => Err(serde_json::Error::invalid_type(
407 unexpected(&it),
408 &"a Vec with an item for each argument",
409 )),
410 Err(e) => Err(e),
411 }
412 }
413}
414
415fn unexpected(v: &serde_json::Value) -> Unexpected<'_> {
416 match v {
417 serde_json::Value::Null => Unexpected::Unit,
418 serde_json::Value::Bool(it) => Unexpected::Bool(*it),
419 serde_json::Value::Number(it) => match (it.as_f64(), it.as_i64(), it.as_u64()) {
420 (None, None, None) => Unexpected::Other("Number"),
421 (Some(it), _, _) => Unexpected::Float(it),
422 (_, Some(it), _) => Unexpected::Signed(it),
423 (_, _, Some(it)) => Unexpected::Unsigned(it),
424 },
425 serde_json::Value::String(it) => Unexpected::Str(it),
426 serde_json::Value::Array(_) => Unexpected::Seq,
427 serde_json::Value::Object(_) => Unexpected::Map,
428 }
429}
430
431macro_rules! do_impls {
432 ($arity:literal $(, $arg:ident)* $(,)?) => {
433 const _: () = {
434 let _assert: [&str; $arity] = [$(stringify!($arg)),*];
435 };
436
437 impl<$($arg),*> Params<$arity> for ($($arg,)*)
438 where
439 $($arg: HasLotusJson + Clone, <$arg as HasLotusJson>::LotusJson: JsonSchema, )*
440 {
441 fn parse(
442 raw: Option<RequestParameters>,
443 arg_names: [&str; $arity],
444 calling_convention: ParamStructure,
445 n_required: usize,
446 ) -> Result<Self, Error> {
447 let mut _parser = Parser::new(raw, &arg_names, calling_convention, n_required)?;
448 Ok(($(_parser.parse::<crate::lotus_json::LotusJson<$arg>>()?.into_inner(),)*))
449 }
450 fn schemas(_gen: &mut SchemaGenerator) -> [(Schema, bool); $arity] {
451 [$((_gen.subschema_for::<$arg::LotusJson>(), $arg::LotusJson::optional())),*]
452 }
453 }
454 };
455}
456
457do_impls!(0);
458do_impls!(1, T0);
459do_impls!(2, T0, T1);
460do_impls!(3, T0, T1, T2);
461do_impls!(4, T0, T1, T2, T3);
462pub enum ConcreteCallingConvention {
472 ByPosition,
473 #[allow(unused)] ByName,
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_api_paths_from_uri() {
483 let v0 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v0".parse().unwrap()).unwrap();
484 assert_eq!(v0, ApiPaths::V0);
485 let v1 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v1".parse().unwrap()).unwrap();
486 assert_eq!(v1, ApiPaths::V1);
487 let v2 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v2".parse().unwrap()).unwrap();
488 assert_eq!(v2, ApiPaths::V2);
489
490 ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v3".parse().unwrap()).unwrap_err();
491 }
492}