Skip to content

Commit 745e933

Browse files
committed
lsp_plugin: add lsps2_buy request and handler
Adds the lsps2.buy request to the client and the lsps2.buy handler to the LSP service. Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
1 parent eb8be4f commit 745e933

File tree

5 files changed

+551
-15
lines changed

5 files changed

+551
-15
lines changed

plugins/lsps-plugin/src/client.rs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
use anyhow::{anyhow, Context};
22
use cln_lsps::jsonrpc::client::JsonRpcClient;
3+
use cln_lsps::lsps0::primitives::Msat;
34
use cln_lsps::lsps0::{
45
self,
56
transport::{Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager},
67
};
7-
use cln_lsps::lsps2::model::{Lsps2GetInfoRequest, Lsps2GetInfoResponse};
8+
use cln_lsps::lsps2::model::{
9+
compute_opening_fee, Lsps2BuyRequest, Lsps2BuyResponse, Lsps2GetInfoRequest,
10+
Lsps2GetInfoResponse, OpeningFeeParams,
11+
};
812
use cln_lsps::util;
913
use cln_lsps::LSP_FEATURE_BIT;
1014
use cln_plugin::options;
1115
use cln_rpc::model::requests::ListpeersRequest;
12-
use cln_rpc::primitives::PublicKey;
16+
use cln_rpc::primitives::{AmountOrAny, PublicKey};
1317
use cln_rpc::ClnRpc;
14-
use log::debug;
18+
use log::{debug, info, warn};
1519
use serde::{Deserialize, Serialize};
1620
use std::path::Path;
1721
use std::str::FromStr as _;
@@ -51,6 +55,11 @@ async fn main() -> Result<(), anyhow::Error> {
5155
"Low-level command to request the opening fee menu of an LSP",
5256
on_lsps_lsps2_getinfo,
5357
)
58+
.rpcmethod(
59+
"lsps-lsps2-buy",
60+
"Low-level command to return the lsps2.buy result from an ",
61+
on_lsps_lsps2_buy,
62+
)
5463
.configure()
5564
.await?
5665
{
@@ -108,6 +117,103 @@ async fn on_lsps_lsps2_getinfo(
108117
Ok(serde_json::to_value(info_res)?)
109118
}
110119

120+
/// Rpc Method handler for `lsps-lsps2-buy`.
121+
async fn on_lsps_lsps2_buy(
122+
p: cln_plugin::Plugin<State>,
123+
v: serde_json::Value,
124+
) -> Result<serde_json::Value, anyhow::Error> {
125+
let req: ClnRpcLsps2BuyRequest =
126+
serde_json::from_value(v).context("Failed to parse request JSON")?;
127+
debug!(
128+
"Asking for a channel from lsp {} with opening fee params {:?} and payment size {:?}",
129+
req.lsp_id, req.opening_fee_params, req.payment_size_msat
130+
);
131+
132+
let dir = p.configuration().lightning_dir;
133+
let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file);
134+
let mut cln_client = cln_rpc::ClnRpc::new(rpc_path.clone()).await?;
135+
136+
// Fail early: Check that we are connected to the peer and that it has the
137+
// LSP feature bit set.
138+
ensure_lsp_connected(&mut cln_client, &req.lsp_id).await?;
139+
140+
// Create Transport and Client
141+
let transport = Bolt8Transport::new(
142+
&req.lsp_id,
143+
rpc_path.clone(), // Clone path for potential reuse
144+
p.state().hook_manager.clone(),
145+
None, // Use default timeout
146+
)
147+
.context("Failed to create Bolt8Transport")?;
148+
let client = JsonRpcClient::new(transport);
149+
150+
// Convert from AmountOrAny to Msat.
151+
let payment_size_msat = if let Some(payment_size) = req.payment_size_msat {
152+
match payment_size {
153+
AmountOrAny::Amount(amount) => Some(Msat::from_msat(amount.msat())),
154+
AmountOrAny::Any => None,
155+
}
156+
} else {
157+
None
158+
};
159+
160+
let selected_params = req.opening_fee_params;
161+
162+
if let Some(payment_size) = payment_size_msat {
163+
if payment_size < selected_params.min_payment_size_msat {
164+
return Err(anyhow!(
165+
"Requested payment size {}msat is below minimum {}msat required by LSP",
166+
payment_size,
167+
selected_params.min_payment_size_msat
168+
));
169+
}
170+
if payment_size > selected_params.max_payment_size_msat {
171+
return Err(anyhow!(
172+
"Requested payment size {}msat is above maximum {}msat allowed by LSP",
173+
payment_size,
174+
selected_params.max_payment_size_msat
175+
));
176+
}
177+
178+
let opening_fee = compute_opening_fee(
179+
payment_size.msat(),
180+
selected_params.min_fee_msat.msat(),
181+
selected_params.proportional.ppm() as u64,
182+
)
183+
.ok_or_else(|| {
184+
warn!(
185+
"Opening fee calculation overflowed for payment size {}",
186+
payment_size
187+
);
188+
anyhow!("failed to calculate opening fee")
189+
})?;
190+
191+
info!(
192+
"Calculated opening fee: {}msat for payment size {}msat",
193+
opening_fee, payment_size
194+
);
195+
} else {
196+
info!("No payment size specified, requesting JIT channel for a variable-amount invoice.");
197+
// Check if the selected params allow for variable amount (implicitly they do if max > min)
198+
if selected_params.min_payment_size_msat >= selected_params.max_payment_size_msat {
199+
// This shouldn't happen if LSP follows spec, but good to check.
200+
warn!("Selected fee params seem unsuitable for variable amount: min >= max");
201+
}
202+
}
203+
204+
debug!("Calling lsps2.buy for peer {}", req.lsp_id);
205+
let buy_req = Lsps2BuyRequest {
206+
opening_fee_params: selected_params, // Pass the chosen params back
207+
payment_size_msat,
208+
};
209+
let buy_res: Lsps2BuyResponse = client
210+
.call_typed(buy_req)
211+
.await
212+
.context("lsps2.buy call failed")?;
213+
214+
Ok(serde_json::to_value(buy_res)?)
215+
}
216+
111217
async fn on_lsps_listprotocols(
112218
p: cln_plugin::Plugin<State>,
113219
v: serde_json::Value,
@@ -188,6 +294,14 @@ async fn ensure_lsp_connected(cln_client: &mut ClnRpc, lsp_id: &str) -> Result<(
188294
Ok(())
189295
}
190296

297+
#[derive(Debug, Clone, Serialize, Deserialize)]
298+
struct ClnRpcLsps2BuyRequest {
299+
lsp_id: String,
300+
#[serde(skip_serializing_if = "Option::is_none")]
301+
payment_size_msat: Option<AmountOrAny>,
302+
opening_fee_params: OpeningFeeParams,
303+
}
304+
191305
#[derive(Debug, Clone, Serialize, Deserialize)]
192306
struct ClnRpcLsps2GetinfoRequest {
193307
lsp_id: String,

0 commit comments

Comments
 (0)