diff --git a/plugins/.gitignore b/plugins/.gitignore index 020252ce6..6d09b413b 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -21,3 +21,4 @@ recklessrpc exposesecret cln-xpay cln-lsps-client +cln-lsps-service diff --git a/plugins/Makefile b/plugins/Makefile index 5618e52a1..4c5cc84b4 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -149,8 +149,10 @@ plugins/clnrest: target/${RUST_PROFILE}/clnrest @cp $< $@ plugins/cln-lsps-client: target/${RUST_PROFILE}/cln-lsps-client @cp $< $@ +plugins/cln-lsps-service: target/${RUST_PROFILE}/cln-lsps-service + @cp $< $@ -PLUGINS += plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client +PLUGINS += plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client plugins/cln-lsps-service endif PLUGIN_COMMON_OBJS := \ @@ -310,10 +312,12 @@ target/${RUST_PROFILE}/clnrest: ${CLN_REST_PLUGIN_SRC} cargo build ${CARGO_OPTS} --bin clnrest target/${RUST_PROFILE}/cln-lsps-client: ${CLN_LSPS_PLUGIN_SRC} cargo build ${CARGO_OPTS} --bin cln-lsps-client +target/${RUST_PROFILE}/cln-lsps-service: ${CLN_LSPS_PLUGIN_SRC} + cargo build ${CARGO_OPTS} --bin cln-lsps-service ifneq ($(RUST),0) include plugins/rest-plugin/Makefile -DEFAULT_TARGETS += $(CLN_PLUGIN_EXAMPLES) plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client +DEFAULT_TARGETS += $(CLN_PLUGIN_EXAMPLES) plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client plugins/cln-lsps-service endif clean: plugins-clean diff --git a/plugins/lsps-plugin/Cargo.toml b/plugins/lsps-plugin/Cargo.toml index e743eeaa2..bbdebe936 100644 --- a/plugins/lsps-plugin/Cargo.toml +++ b/plugins/lsps-plugin/Cargo.toml @@ -7,6 +7,10 @@ edition = "2021" name = "cln-lsps-client" path = "src/client.rs" +[[bin]] +name = "cln-lsps-service" +path = "src/service.rs" + [dependencies] anyhow = "1.0" async-trait = "0.1" diff --git a/plugins/lsps-plugin/src/service.rs b/plugins/lsps-plugin/src/service.rs new file mode 100644 index 000000000..8078f0043 --- /dev/null +++ b/plugins/lsps-plugin/src/service.rs @@ -0,0 +1,126 @@ +use anyhow::anyhow; +use async_trait::async_trait; +use cln_lsps::jsonrpc::server::{JsonRpcResponseWriter, RequestHandler}; +use cln_lsps::jsonrpc::{server::JsonRpcServer, JsonRpcRequest}; +use cln_lsps::jsonrpc::{JsonRpcResponse, RequestObject, RpcError, TransportError}; +use cln_lsps::lsps0; +use cln_lsps::lsps0::model::{Lsps0listProtocolsRequest, Lsps0listProtocolsResponse}; +use cln_lsps::lsps0::transport::{self, CustomMsg}; +use cln_plugin::options::ConfigOption; +use cln_plugin::{options, Plugin}; +use cln_rpc::notifications::CustomMsgNotification; +use cln_rpc::primitives::PublicKey; +use log::debug; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; + +/// An option to enable this service. It defaults to `false` as we don't want a +/// node to be an LSP per default. +/// If a user want's to run an LSP service on their node this has to explicitly +/// set to true. We keep this as a dev option for now until it actually does +/// something. +const OPTION_ENABLED: options::DefaultBooleanConfigOption = ConfigOption::new_bool_with_default( + "dev-lsps-service", + false, + "Enables an LSPS service on the node.", +); + +#[derive(Clone)] +struct State { + lsps_service: JsonRpcServer, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let lsps_service = JsonRpcServer::builder() + .with_handler( + Lsps0listProtocolsRequest::METHOD.to_string(), + Arc::new(Lsps0ListProtocolsHandler), + ) + .build(); + let state = State { lsps_service }; + + if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout()) + .option(OPTION_ENABLED) + .hook("custommsg", on_custommsg) + .configure() + .await? + { + if !plugin.option(&OPTION_ENABLED)? { + return plugin + .disable(&format!("`{}` not enabled", OPTION_ENABLED.name)) + .await; + } + + let plugin = plugin.start(state).await?; + plugin.join().await + } else { + Ok(()) + } +} + +async fn on_custommsg( + p: Plugin, + v: serde_json::Value, +) -> Result { + // All of this could be done async if needed. + let continue_response = Ok(serde_json::json!({ + "result": "continue" + })); + let msg: CustomMsgNotification = + serde_json::from_value(v).map_err(|e| anyhow!("invalid custommsg: {e}"))?; + + let req = CustomMsg::from_str(&msg.payload).map_err(|e| anyhow!("invalid payload {e}"))?; + if req.message_type != lsps0::transport::LSPS0_MESSAGE_TYPE { + // We don't care if this is not for us! + return continue_response; + } + + let dir = p.configuration().lightning_dir; + let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file); + let mut writer = LspsResponseWriter { + peer_id: msg.peer_id, + rpc_path: rpc_path.try_into()?, + }; + + let service = p.state().lsps_service.clone(); + match service.handle_message(&req.payload, &mut writer).await { + Ok(_) => continue_response, + Err(e) => { + debug!("failed to handle lsps message: {}", e); + continue_response + } + } +} + +pub struct LspsResponseWriter { + peer_id: PublicKey, + rpc_path: PathBuf, +} + +#[async_trait] +impl JsonRpcResponseWriter for LspsResponseWriter { + async fn write(&mut self, payload: &[u8]) -> cln_lsps::jsonrpc::Result<()> { + let mut client = cln_rpc::ClnRpc::new(&self.rpc_path).await.map_err(|e| { + cln_lsps::jsonrpc::Error::Transport(TransportError::Other(e.to_string())) + })?; + transport::send_custommsg(&mut client, payload.to_vec(), self.peer_id).await + } +} + +pub struct Lsps0ListProtocolsHandler; + +#[async_trait] +impl RequestHandler for Lsps0ListProtocolsHandler { + async fn handle(&self, payload: &[u8]) -> core::result::Result, RpcError> { + let req: RequestObject = + serde_json::from_slice(payload).unwrap(); + if let Some(id) = req.id { + let res = Lsps0listProtocolsResponse { protocols: vec![] }.into_response(id); + let res_vec = serde_json::to_vec(&res).unwrap(); + return Ok(res_vec); + } + Ok(vec![]) + } +}