|
1 | 1 | use anyhow::{anyhow, Context}; |
2 | 2 | use cln_lsps::jsonrpc::client::JsonRpcClient; |
| 3 | +use cln_lsps::lsps0::primitives::Msat; |
3 | 4 | use cln_lsps::lsps0::{ |
4 | 5 | self, |
5 | 6 | transport::{Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager}, |
6 | 7 | }; |
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 | +}; |
8 | 12 | use cln_lsps::util; |
9 | 13 | use cln_lsps::LSP_FEATURE_BIT; |
10 | 14 | use cln_plugin::options; |
11 | 15 | use cln_rpc::model::requests::ListpeersRequest; |
12 | | -use cln_rpc::primitives::PublicKey; |
| 16 | +use cln_rpc::primitives::{AmountOrAny, PublicKey}; |
13 | 17 | use cln_rpc::ClnRpc; |
14 | | -use log::debug; |
| 18 | +use log::{debug, info, warn}; |
15 | 19 | use serde::{Deserialize, Serialize}; |
16 | 20 | use std::path::Path; |
17 | 21 | use std::str::FromStr as _; |
@@ -51,6 +55,11 @@ async fn main() -> Result<(), anyhow::Error> { |
51 | 55 | "Low-level command to request the opening fee menu of an LSP", |
52 | 56 | on_lsps_lsps2_getinfo, |
53 | 57 | ) |
| 58 | + .rpcmethod( |
| 59 | + "lsps-lsps2-buy", |
| 60 | + "Low-level command to return the lsps2.buy result from an ", |
| 61 | + on_lsps_lsps2_buy, |
| 62 | + ) |
54 | 63 | .configure() |
55 | 64 | .await? |
56 | 65 | { |
@@ -108,6 +117,103 @@ async fn on_lsps_lsps2_getinfo( |
108 | 117 | Ok(serde_json::to_value(info_res)?) |
109 | 118 | } |
110 | 119 |
|
| 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 | + |
111 | 217 | async fn on_lsps_listprotocols( |
112 | 218 | p: cln_plugin::Plugin<State>, |
113 | 219 | v: serde_json::Value, |
@@ -188,6 +294,14 @@ async fn ensure_lsp_connected(cln_client: &mut ClnRpc, lsp_id: &str) -> Result<( |
188 | 294 | Ok(()) |
189 | 295 | } |
190 | 296 |
|
| 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 | + |
191 | 305 | #[derive(Debug, Clone, Serialize, Deserialize)] |
192 | 306 | struct ClnRpcLsps2GetinfoRequest { |
193 | 307 | lsp_id: String, |
|
0 commit comments