breez_sdk_core/
lsp.rs

1use crate::crypt::encrypt;
2use crate::error::{SdkError, SdkResult};
3use crate::models::{LspAPI, OpeningFeeParams, OpeningFeeParamsMenu};
4
5use anyhow::{anyhow, Result};
6use prost::Message;
7use sdk_common::grpc::{
8    self, LspFullListRequest, LspListRequest, PaymentInformation,
9    RegisterPaymentNotificationRequest, RegisterPaymentNotificationResponse, RegisterPaymentReply,
10    RegisterPaymentRequest, RemovePaymentNotificationRequest, RemovePaymentNotificationResponse,
11    SubscribeNotificationsRequest, UnsubscribeNotificationsRequest,
12};
13use sdk_common::prelude::BreezServer;
14use sdk_common::with_connection_retry;
15use serde::{Deserialize, Serialize};
16
17/// Details of supported LSP
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct LspInformation {
20    pub id: String,
21
22    /// The name of of LSP
23    pub name: String,
24
25    /// The URL of the LSP
26    pub widget_url: String,
27
28    /// The identity pubkey of the Lightning node
29    pub pubkey: String,
30
31    /// The network location of the lightning node, e.g. `12.34.56.78:9012` or `localhost:10011`
32    pub host: String,
33
34    /// The base fee charged regardless of the number of milli-satoshis sent
35    pub base_fee_msat: i64,
36
37    /// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6.
38    pub fee_rate: f64,
39
40    /// The required timelock delta for HTLCs forwarded over the channel
41    pub time_lock_delta: u32,
42
43    /// The minimum value in millisatoshi we will require for incoming HTLCs on the channel
44    pub min_htlc_msat: i64,
45    pub lsp_pubkey: Vec<u8>,
46    pub opening_fee_params_list: OpeningFeeParamsMenu,
47}
48
49impl LspInformation {
50    /// Validation may fail if [LspInformation.opening_fee_params_list] has invalid entries
51    fn try_from(lsp_id: &str, lsp_info: grpc::LspInformation) -> Result<Self> {
52        let info = LspInformation {
53            id: lsp_id.to_string(),
54            name: lsp_info.name,
55            widget_url: lsp_info.widget_url,
56            pubkey: lsp_info.pubkey,
57            host: lsp_info.host,
58            base_fee_msat: lsp_info.base_fee_msat,
59            fee_rate: lsp_info.fee_rate,
60            time_lock_delta: lsp_info.time_lock_delta,
61            min_htlc_msat: lsp_info.min_htlc_msat,
62            lsp_pubkey: lsp_info.lsp_pubkey,
63            opening_fee_params_list: OpeningFeeParamsMenu::try_from(
64                lsp_info.opening_fee_params_menu,
65            )?,
66        };
67
68        Ok(info)
69    }
70
71    /// Returns the cheapest opening channel fees from LSP that within the expiry range.
72    ///
73    /// If the LSP fees are needed, the LSP is expected to have at least one dynamic fee entry in its menu,
74    /// otherwise this will result in an error.
75    pub(crate) fn cheapest_open_channel_fee(&self, expiry: u32) -> Result<&OpeningFeeParams> {
76        for fee in &self.opening_fee_params_list.values {
77            match fee.valid_for(expiry) {
78                Ok(valid) => {
79                    if valid {
80                        return Ok(fee);
81                    }
82                }
83                Err(e) => return Err(anyhow!("Failed to calculate open channel fees: {e}")),
84            }
85        }
86        self.opening_fee_params_list
87            .values
88            .last()
89            .ok_or_else(|| anyhow!("Dynamic fees menu contains no values"))
90    }
91}
92
93#[tonic::async_trait]
94impl LspAPI for BreezServer {
95    async fn list_lsps(&self, pubkey: String) -> SdkResult<Vec<LspInformation>> {
96        let mut client = self.get_channel_opener_client().await?;
97
98        let request = LspListRequest { pubkey };
99        let response = with_connection_retry!(client.lsp_list(request.clone())).await?;
100        let mut lsp_list: Vec<LspInformation> = Vec::new();
101        for (lsp_id, lsp_info) in response.into_inner().lsps.into_iter() {
102            match LspInformation::try_from(&lsp_id, lsp_info) {
103                Ok(lsp) => lsp_list.push(lsp),
104                Err(e) => error!("LSP Information validation failed for LSP {lsp_id}: {e}"),
105            }
106        }
107        lsp_list.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
108        Ok(lsp_list)
109    }
110
111    async fn list_used_lsps(&self, pubkey: String) -> SdkResult<Vec<LspInformation>> {
112        let mut client = self.get_channel_opener_client().await?;
113
114        let request = LspFullListRequest { pubkey };
115        let response = with_connection_retry!(client.lsp_full_list(request.clone())).await?;
116        let mut lsp_list: Vec<LspInformation> = Vec::new();
117        for grpc_lsp_info in response.into_inner().lsps.into_iter() {
118            let lsp_id = grpc_lsp_info.id.clone();
119            match LspInformation::try_from(&lsp_id, grpc_lsp_info) {
120                Ok(lsp) => lsp_list.push(lsp),
121                Err(e) => error!("LSP Information validation failed for LSP {lsp_id}: {e}"),
122            }
123        }
124        lsp_list.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
125        Ok(lsp_list)
126    }
127
128    async fn register_payment_notifications(
129        &self,
130        lsp_id: String,
131        lsp_pubkey: Vec<u8>,
132        webhook_url: String,
133        webhook_url_signature: String,
134    ) -> SdkResult<RegisterPaymentNotificationResponse> {
135        let subscribe_request = SubscribeNotificationsRequest {
136            url: webhook_url,
137            signature: webhook_url_signature,
138        };
139
140        let mut client = self.get_payment_notifier_client().await;
141
142        let mut buf = Vec::with_capacity(subscribe_request.encoded_len());
143        subscribe_request
144            .encode(&mut buf)
145            .map_err(|e| SdkError::Generic {
146                err: format!("(LSP {lsp_id}) Failed to encode subscribe request: {e}"),
147            })?;
148
149        let request = RegisterPaymentNotificationRequest {
150            lsp_id,
151            blob: encrypt(lsp_pubkey, buf)?,
152        };
153        let response =
154            with_connection_retry!(client.register_payment_notification(request.clone())).await?;
155
156        Ok(response.into_inner())
157    }
158
159    async fn unregister_payment_notifications(
160        &self,
161        lsp_id: String,
162        lsp_pubkey: Vec<u8>,
163        webhook_url: String,
164        webhook_url_signature: String,
165    ) -> SdkResult<RemovePaymentNotificationResponse> {
166        let unsubscribe_request = UnsubscribeNotificationsRequest {
167            url: webhook_url,
168            signature: webhook_url_signature,
169        };
170
171        let mut client = self.get_payment_notifier_client().await;
172
173        let mut buf = Vec::with_capacity(unsubscribe_request.encoded_len());
174        unsubscribe_request
175            .encode(&mut buf)
176            .map_err(|e| SdkError::Generic {
177                err: format!("(LSP {lsp_id}) Failed to encode unsubscribe request: {e}"),
178            })?;
179
180        let request = RemovePaymentNotificationRequest {
181            lsp_id,
182            blob: encrypt(lsp_pubkey, buf)?,
183        };
184        let response =
185            with_connection_retry!(client.remove_payment_notification(request.clone())).await?;
186
187        Ok(response.into_inner())
188    }
189
190    async fn register_payment(
191        &self,
192        lsp_id: String,
193        lsp_pubkey: Vec<u8>,
194        payment_info: PaymentInformation,
195    ) -> SdkResult<RegisterPaymentReply> {
196        let mut client = self.get_channel_opener_client().await?;
197
198        let mut buf = Vec::with_capacity(payment_info.encoded_len());
199        payment_info
200            .encode(&mut buf)
201            .map_err(|e| SdkError::ServiceConnectivity {
202                err: format!("(LSP {lsp_id}) Failed to encode payment info: {e}"),
203            })?;
204
205        let request = RegisterPaymentRequest {
206            lsp_id,
207            blob: encrypt(lsp_pubkey, buf)?,
208        };
209        let response = with_connection_retry!(client.register_payment(request.clone())).await?;
210
211        Ok(response.into_inner())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use crate::{LspInformation, OpeningFeeParams};
218
219    use super::OpeningFeeParamsMenu;
220    use anyhow::Result;
221    use chrono::{Duration, Utc};
222
223    #[test]
224    fn test_cheapest_open_channel_fee() -> Result<()> {
225        let mut tested_fees: Vec<OpeningFeeParams> = vec![];
226        for i in 1..3 {
227            tested_fees.push(OpeningFeeParams {
228                min_msat: i,
229                proportional: i as u32,
230                valid_until: std::ops::Add::add(Utc::now(), Duration::seconds((i * 3600) as i64))
231                    .to_rfc3339(),
232                max_idle_time: i as u32,
233                max_client_to_self_delay: i as u32,
234                promise: format!("promise {i}"),
235            })
236        }
237
238        let mut lsp_info = LspInformation {
239            id: "id".to_string(),
240            name: "test lsp".to_string(),
241            widget_url: "".to_string(),
242            pubkey: "pubkey".to_string(),
243            host: "localhost".to_string(),
244            base_fee_msat: 1,
245            fee_rate: 1.0,
246            time_lock_delta: 32,
247            min_htlc_msat: 1000,
248            lsp_pubkey: hex::decode("A0").unwrap(),
249            opening_fee_params_list: OpeningFeeParamsMenu {
250                values: tested_fees,
251            },
252        };
253
254        for expiry in 1..3 {
255            let fee = lsp_info
256                .cheapest_open_channel_fee(expiry * 3600 - 1000)
257                .unwrap();
258            assert_eq!(fee.min_msat, expiry as u64);
259        }
260
261        // Test that the fee is returned even after the expiry
262        let fee = lsp_info.cheapest_open_channel_fee(4 * 3600 - 1000).unwrap();
263        assert_eq!(fee.min_msat, 2);
264
265        // Test the correct error when there are no fees in the menu
266        lsp_info.opening_fee_params_list = OpeningFeeParamsMenu { values: vec![] };
267        let err = lsp_info.cheapest_open_channel_fee(4 * 3600).err().unwrap();
268        assert_eq!(err.to_string(), "Dynamic fees menu contains no values");
269
270        Ok(())
271    }
272}