Files
palladum-lightning/plugins/lsps-plugin/src/service.rs
Peter Neuroth ce0be9dc87 lsp_plugin: remove redundant config option
We don't need to separately enable lsp and lsps2 services. If lsps2 is
not enabled what can we do with just the messaging layer?

Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
2025-11-13 10:58:49 +10:30

190 lines
6.9 KiB
Rust

use anyhow::{anyhow, bail};
use async_trait::async_trait;
use cln_lsps::jsonrpc::server::JsonRpcResponseWriter;
use cln_lsps::jsonrpc::TransportError;
use cln_lsps::jsonrpc::{server::JsonRpcServer, JsonRpcRequest};
use cln_lsps::lsps0::handler::Lsps0ListProtocolsHandler;
use cln_lsps::lsps0::model::Lsps0listProtocolsRequest;
use cln_lsps::lsps0::transport::{self, CustomMsg};
use cln_lsps::lsps2::cln::{HtlcAcceptedRequest, HtlcAcceptedResponse};
use cln_lsps::lsps2::handler::{ClnApiRpc, HtlcAcceptedHookHandler};
use cln_lsps::lsps2::model::{Lsps2BuyRequest, Lsps2GetInfoRequest};
use cln_lsps::util::wrap_payload_with_peer_id;
use cln_lsps::{lsps0, lsps2, util, LSP_FEATURE_BIT};
use cln_plugin::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;
#[derive(Clone)]
struct State {
lsps_service: JsonRpcServer,
lsps2_enabled: bool,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
.option(lsps2::OPTION_ENABLED)
.option(lsps2::OPTION_PROMISE_SECRET)
.featurebits(
cln_plugin::FeatureBitsKind::Node,
util::feature_bit_to_hex(LSP_FEATURE_BIT),
)
.featurebits(
cln_plugin::FeatureBitsKind::Init,
util::feature_bit_to_hex(LSP_FEATURE_BIT),
)
.hook("custommsg", on_custommsg)
.hook("htlc_accepted", on_htlc_accepted)
.configure()
.await?
{
let rpc_path =
Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file);
if plugin.option(&lsps2::OPTION_ENABLED)? {
log::debug!("lsps2-service enabled");
if let Some(secret_hex) = plugin.option(&lsps2::OPTION_PROMISE_SECRET)? {
let secret_hex = secret_hex.trim().to_lowercase();
let decoded_bytes = match hex::decode(&secret_hex) {
Ok(bytes) => bytes,
Err(_) => {
return plugin
.disable(&format!(
"Invalid hex string for promise secret: {}",
secret_hex
))
.await;
}
};
let secret: [u8; 32] = match decoded_bytes.try_into() {
Ok(array) => array,
Err(vec) => {
return plugin
.disable(&format!(
"Promise secret must be exactly 32 bytes, got {}",
vec.len()
))
.await;
}
};
let mut lsps_builder = JsonRpcServer::builder().with_handler(
Lsps0listProtocolsRequest::METHOD.to_string(),
Arc::new(Lsps0ListProtocolsHandler {
lsps2_enabled: plugin.option(&lsps2::OPTION_ENABLED)?,
}),
);
let cln_api_rpc = lsps2::handler::ClnApiRpc::new(rpc_path);
let getinfo_handler =
lsps2::handler::Lsps2GetInfoHandler::new(cln_api_rpc.clone(), secret);
let buy_handler = lsps2::handler::Lsps2BuyHandler::new(cln_api_rpc, secret);
lsps_builder = lsps_builder
.with_handler(
Lsps2GetInfoRequest::METHOD.to_string(),
Arc::new(getinfo_handler),
)
.with_handler(Lsps2BuyRequest::METHOD.to_string(), Arc::new(buy_handler));
let lsps_service = lsps_builder.build();
let state = State {
lsps_service,
lsps2_enabled: true,
};
let plugin = plugin.start(state).await?;
plugin.join().await
} else {
bail!("lsps2 enabled but no promise-secret set.");
}
} else {
return plugin
.disable(&format!("`{}` not enabled", &lsps2::OPTION_ENABLED.name))
.await;
}
} else {
Ok(())
}
}
async fn on_htlc_accepted(
p: Plugin<State>,
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
if !p.state().lsps2_enabled {
// just continue.
// Fixme: Add forward and extra tlvs from incoming.
let res = serde_json::to_value(&HtlcAcceptedResponse::continue_(None, None, None))?;
return Ok(res);
}
let req: HtlcAcceptedRequest = serde_json::from_value(v)?;
let rpc_path = Path::new(&p.configuration().lightning_dir).join(&p.configuration().rpc_file);
let api = ClnApiRpc::new(rpc_path);
// Fixme: Use real htlc_minimum_amount.
let handler = HtlcAcceptedHookHandler::new(api, 1000);
let res = handler.handle(req).await?;
let res_val = serde_json::to_value(&res)?;
Ok(res_val)
}
async fn on_custommsg(
p: Plugin<State>,
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
// 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()?,
};
// The payload inside CustomMsg is the actual JSON-RPC
// request/notification, we wrap it to attach the peer_id as well.
let payload = wrap_payload_with_peer_id(&req.payload, msg.peer_id);
let service = p.state().lsps_service.clone();
match service.handle_message(&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
}
}