/// The types used to build the `help` documentation for a module, as well as determine the /// arguments that can be automatically detected by Icepick. pub mod help { use serde::{Deserialize, Serialize}; /// An operation that can be exposed to a frontend. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Operation { /// The name of the argument, in the format expected by the deserializer. pub name: String, /// A description of what the operation does. pub description: String, /// The arguments to be provided to the operation's deserializer. pub arguments: Vec, } #[bon::bon] impl Operation { #[builder] pub fn new(name: &'static str, description: &'static str) -> Self { Operation { name: name.into(), description: description.into(), arguments: vec![], } } } impl Operation { pub fn argument(mut self, arg: &Argument) -> Self { self.arguments.push(arg.clone()); self } } /* /// The context of whether a signature is signed, needs to be signed, or has been signed. #[derive(Serialize, Deserialize, Clone)] pub enum SigningContext { /// This operation accepts a signed blob. Signed, /// This operation accepts an unsigned blob and will return a signed blob. Signer, /// This operation will return an unsigned blob. Unsigned, } */ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ArgumentType { Required, Optional, } /// An argument to an operation. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Argument { /// The name of the argument, in the format expected by the deserializer. pub name: String, /// A description of the format and parameters of an argument. pub description: String, /// The type of argument - this may affect how it displays in the frontend. pub r#type: ArgumentType, } #[bon::bon] impl Argument { #[builder] pub fn new(name: &'static str, description: &'static str, r#type: ArgumentType) -> Self { Argument { name: name.into(), description: description.into(), r#type, } } } } /// Implementation methods for Icepick Modules, performed over command I/O using JSON. pub trait Module { /// The error type returned by an operation. type Error: std::error::Error + 'static; /// The request type. See [`Module::handle_request`] for more information. type Request: serde::de::DeserializeOwned + std::fmt::Debug + 'static; /// Describe the operations interface of RPC calls. This information will be used to generate a /// frontend for users to perform requests. fn describe_operations() -> Vec; /// Handle an incoming request. Requests can often be formed using the following schema: /// /// ```rust /// #[derive(serde::Serialize, serde::Deserialize, Debug)] /// #[serde(tag = "operation", rename_all="kebab-case")] /// enum Request { /// Transfer { /// // This is the amount of the "primary" currency you'd be receiving, such as Ether, /// // Bitcoin, or Sol. /// amount: f64, /// /// // This would be your native wallet address type. In this example, we'll just use a /// // String. /// to_address: String, /// /// // This is the address of the derivation account. /// from_account: u32, /// } /// /// Stake { /// amount: f64, /// from_account: u32, /// } /// /// Sign { /// blob: /// } /// } /// ``` fn handle_request(request: Self::Request) -> Result; fn run_responder() -> Result<(), Box> { let mut lines = std::io::stdin().lines(); while let Some(line) = lines.next().transpose()? { let value: serde_json::Value = serde_json::from_str(&line)?; if let Some(serde_json::Value::String(operation)) = value.get("operation") { if operation == "help" { println!("{}", serde_json::to_string(&Self::describe_operations())?); continue; } if operation == "exit" { break; } } // TODO: error handling let request: Self::Request = serde_json::from_value(value).expect("good value"); let response = Self::handle_request(request)?; println!("{}", serde_json::to_string(&response).unwrap()); } Ok(()) } }