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#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct LspInformation {
20 pub id: String,
21
22 pub name: String,
24
25 pub widget_url: String,
27
28 pub pubkey: String,
30
31 pub host: String,
33
34 pub base_fee_msat: i64,
36
37 pub fee_rate: f64,
39
40 pub time_lock_delta: u32,
42
43 pub min_htlc_msat: i64,
45 pub lsp_pubkey: Vec<u8>,
46 pub opening_fee_params_list: OpeningFeeParamsMenu,
47}
48
49impl LspInformation {
50 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 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 let fee = lsp_info.cheapest_open_channel_fee(4 * 3600 - 1000).unwrap();
263 assert_eq!(fee.min_msat, 2);
264
265 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}