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 displaydoc::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(
162 uri.path().split("/").last().expect("infallible"),
163 )?)
164 }
165}
166
167pub trait RpcMethodExt<const ARITY: usize>: RpcMethod<ARITY> {
170 fn build_params(
174 params: Self::Params,
175 calling_convention: ConcreteCallingConvention,
176 ) -> Result<RequestParameters, serde_json::Error> {
177 let args = params.unparse()?;
178 match calling_convention {
179 ConcreteCallingConvention::ByPosition => {
180 Ok(RequestParameters::ByPosition(Vec::from(args)))
181 }
182 ConcreteCallingConvention::ByName => Ok(RequestParameters::ByName(
183 itertools::zip_eq(Self::PARAM_NAMES.into_iter().map(String::from), args).collect(),
184 )),
185 }
186 }
187
188 fn parse_params(
189 params_raw: Option<impl AsRef<str>>,
190 calling_convention: ParamStructure,
191 ) -> anyhow::Result<Self::Params> {
192 Ok(Self::Params::parse(
193 params_raw
194 .map(|s| serde_json::from_str(s.as_ref()))
195 .transpose()?,
196 Self::PARAM_NAMES,
197 calling_convention,
198 Self::N_REQUIRED_PARAMS,
199 )?)
200 }
201
202 fn openrpc<'de>(
204 g: &mut SchemaGenerator,
205 calling_convention: ParamStructure,
206 method_name: &'static str,
207 ) -> Method
208 where
209 <Self::Ok as HasLotusJson>::LotusJson: JsonSchema + Deserialize<'de>,
210 {
211 Method {
212 name: String::from(method_name),
213 params: itertools::zip_eq(Self::PARAM_NAMES, Self::Params::schemas(g))
214 .enumerate()
215 .map(|(pos, (name, (schema, nullable)))| {
216 let required = pos <= Self::N_REQUIRED_PARAMS;
217 if !required && !nullable {
218 panic!("Optional parameter at position {pos} should be of an optional type. method={method_name}, param_name={name}");
219 }
220 ReferenceOr::Item(ContentDescriptor {
221 name: String::from(name),
222 schema,
223 required: Some(required),
224 ..Default::default()
225 })
226 })
227 .collect(),
228 param_structure: Some(calling_convention),
229 result: Some(ReferenceOr::Item(ContentDescriptor {
230 name: format!("{method_name}.Result"),
231 schema: g.subschema_for::<<Self::Ok as HasLotusJson>::LotusJson>(),
232 required: Some(!<Self::Ok as HasLotusJson>::LotusJson::optional()),
233 ..Default::default()
234 })),
235 summary: Self::SUMMARY.map(Into::into),
236 description: Self::DESCRIPTION.map(Into::into),
237 ..Default::default()
238 }
239 }
240
241 fn register(
243 modules: &mut HashMap<
244 ApiPaths,
245 RpcModule<crate::rpc::RPCState<impl Blockstore + Send + Sync + 'static>>,
246 >,
247 calling_convention: ParamStructure,
248 ) -> Result<(), jsonrpsee::core::RegisterMethodError>
249 where
250 <Self::Ok as HasLotusJson>::LotusJson: Clone + 'static,
251 {
252 use clap::ValueEnum as _;
253
254 assert!(
255 Self::N_REQUIRED_PARAMS <= ARITY,
256 "N_REQUIRED_PARAMS({}) can not be greater than ARITY({ARITY}) in {}",
257 Self::N_REQUIRED_PARAMS,
258 Self::NAME
259 );
260
261 for api_version in ApiPaths::value_variants() {
262 if Self::API_PATHS.contains(*api_version)
263 && let Some(module) = modules.get_mut(api_version)
264 {
265 module.register_async_method(
266 Self::NAME,
267 move |params, ctx, _extensions| async move {
268 let params = Self::parse_params(params.as_str(), calling_convention)
269 .map_err(|e| Error::invalid_params(e, None))?;
270 let ok = Self::handle(ctx, params).await?;
271 Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json())
272 },
273 )?;
274 if let Some(alias) = Self::NAME_ALIAS {
275 module.register_alias(alias, Self::NAME)?
276 }
277 }
278 }
279 Ok(())
280 }
281 fn request(params: Self::Params) -> Result<crate::rpc::Request<Self::Ok>, serde_json::Error> {
283 let params = Self::request_params(params)?;
285 Ok(crate::rpc::Request {
286 method_name: Self::NAME.into(),
287 params,
288 result_type: std::marker::PhantomData,
289 api_paths: Self::API_PATHS,
290 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
291 })
292 }
293
294 fn request_params(params: Self::Params) -> Result<serde_json::Value, serde_json::Error> {
295 Ok(
297 match Self::build_params(params, ConcreteCallingConvention::ByPosition)? {
298 RequestParameters::ByPosition(mut it) => {
299 while Self::N_REQUIRED_PARAMS < it.len() {
303 match it.last() {
304 Some(last) if last.is_null() => it.pop(),
305 _ => break,
306 };
307 }
308 serde_json::Value::Array(it)
309 }
310 RequestParameters::ByName(it) => serde_json::Value::Object(it),
311 },
312 )
313 }
314
315 fn request_with_alias(
317 params: Self::Params,
318 use_alias: bool,
319 ) -> anyhow::Result<crate::rpc::Request<Self::Ok>> {
320 let params = Self::request_params(params)?;
321 let name = if use_alias {
322 Self::NAME_ALIAS.context("alias is None")?
323 } else {
324 Self::NAME
325 };
326
327 Ok(crate::rpc::Request {
328 method_name: name.into(),
329 params,
330 result_type: std::marker::PhantomData,
331 api_paths: Self::API_PATHS,
332 timeout: *crate::rpc::DEFAULT_REQUEST_TIMEOUT,
333 })
334 }
335 fn call_raw(
336 client: &crate::rpc::client::Client,
337 params: Self::Params,
338 ) -> impl Future<Output = Result<<Self::Ok as HasLotusJson>::LotusJson, jsonrpsee::core::ClientError>>
339 {
340 async {
341 let json = client.call(Self::request(params)?.map_ty()).await?;
345 Ok(serde_json::from_value(json)?)
346 }
347 }
348 fn call(
349 client: &crate::rpc::client::Client,
350 params: Self::Params,
351 ) -> impl Future<Output = Result<Self::Ok, jsonrpsee::core::ClientError>> {
352 async {
353 Self::call_raw(client, params)
354 .await
355 .map(Self::Ok::from_lotus_json)
356 }
357 }
358}
359impl<const ARITY: usize, T> RpcMethodExt<ARITY> for T where T: RpcMethod<ARITY> {}
360
361pub trait Params<const ARITY: usize>: HasLotusJson {
365 fn schemas(g: &mut SchemaGenerator) -> [(Schema, bool); ARITY];
368 fn parse(
371 raw: Option<RequestParameters>,
372 names: [&str; ARITY],
373 calling_convention: ParamStructure,
374 n_required: usize,
375 ) -> Result<Self, Error>
376 where
377 Self: Sized;
378 fn unparse(self) -> Result<[serde_json::Value; ARITY], serde_json::Error> {
382 match self.into_lotus_json_value() {
383 Ok(serde_json::Value::Array(args)) => match args.try_into() {
384 Ok(it) => Ok(it),
385 Err(_) => Err(serde_json::Error::custom("ARITY mismatch")),
386 },
387 Ok(serde_json::Value::Null) if ARITY == 0 => {
388 Ok(std::array::from_fn(|_ix| Default::default()))
389 }
390 Ok(it) => Err(serde_json::Error::invalid_type(
391 unexpected(&it),
392 &"a Vec with an item for each argument",
393 )),
394 Err(e) => Err(e),
395 }
396 }
397}
398
399fn unexpected(v: &serde_json::Value) -> Unexpected<'_> {
400 match v {
401 serde_json::Value::Null => Unexpected::Unit,
402 serde_json::Value::Bool(it) => Unexpected::Bool(*it),
403 serde_json::Value::Number(it) => match (it.as_f64(), it.as_i64(), it.as_u64()) {
404 (None, None, None) => Unexpected::Other("Number"),
405 (Some(it), _, _) => Unexpected::Float(it),
406 (_, Some(it), _) => Unexpected::Signed(it),
407 (_, _, Some(it)) => Unexpected::Unsigned(it),
408 },
409 serde_json::Value::String(it) => Unexpected::Str(it),
410 serde_json::Value::Array(_) => Unexpected::Seq,
411 serde_json::Value::Object(_) => Unexpected::Map,
412 }
413}
414
415macro_rules! do_impls {
416 ($arity:literal $(, $arg:ident)* $(,)?) => {
417 const _: () = {
418 let _assert: [&str; $arity] = [$(stringify!($arg)),*];
419 };
420
421 impl<$($arg),*> Params<$arity> for ($($arg,)*)
422 where
423 $($arg: HasLotusJson + Clone, <$arg as HasLotusJson>::LotusJson: JsonSchema, )*
424 {
425 fn parse(
426 raw: Option<RequestParameters>,
427 arg_names: [&str; $arity],
428 calling_convention: ParamStructure,
429 n_required: usize,
430 ) -> Result<Self, Error> {
431 let mut _parser = Parser::new(raw, &arg_names, calling_convention, n_required)?;
432 Ok(($(_parser.parse::<crate::lotus_json::LotusJson<$arg>>()?.into_inner(),)*))
433 }
434 fn schemas(_gen: &mut SchemaGenerator) -> [(Schema, bool); $arity] {
435 [$((_gen.subschema_for::<$arg::LotusJson>(), $arg::LotusJson::optional())),*]
436 }
437 }
438 };
439}
440
441do_impls!(0);
442do_impls!(1, T0);
443do_impls!(2, T0, T1);
444do_impls!(3, T0, T1, T2);
445do_impls!(4, T0, T1, T2, T3);
446pub enum ConcreteCallingConvention {
456 ByPosition,
457 #[allow(unused)] ByName,
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_api_paths_from_uri() {
467 let v0 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v0".parse().unwrap()).unwrap();
468 assert_eq!(v0, ApiPaths::V0);
469 let v1 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v1".parse().unwrap()).unwrap();
470 assert_eq!(v1, ApiPaths::V1);
471 let v2 = ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v2".parse().unwrap()).unwrap();
472 assert_eq!(v2, ApiPaths::V2);
473
474 ApiPaths::from_uri(&"http://127.0.0.1:2345/rpc/v3".parse().unwrap()).unwrap_err();
475 }
476}