1use std::fmt::Debug;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use fedimint_core::task::TaskGroup;
6use fedimint_core::util::SafeUrl;
7use fedimint_core::Amount;
8use fedimint_ln_common::PrunedInvoice;
9use futures::stream::StreamExt;
10use reqwest::Method;
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13use tracing::info;
14
15use super::{ChannelInfo, ILnRpcClient, LightningRpcError, RouteHtlcStream};
16use crate::lightning::extension::{
17 CLN_CLOSE_CHANNELS_WITH_PEER_ENDPOINT, CLN_COMPLETE_PAYMENT_ENDPOINT,
18 CLN_CREATE_INVOICE_ENDPOINT, CLN_GET_BALANCES_ENDPOINT, CLN_INFO_ENDPOINT,
19 CLN_LIST_ACTIVE_CHANNELS_ENDPOINT, CLN_LN_ONCHAIN_ADDRESS_ENDPOINT, CLN_OPEN_CHANNEL_ENDPOINT,
20 CLN_PAY_PRUNED_INVOICE_ENDPOINT, CLN_ROUTE_HINTS_ENDPOINT, CLN_ROUTE_HTLCS_ENDPOINT,
21 CLN_SEND_ONCHAIN_ENDPOINT,
22};
23use crate::lightning::{
24 CloseChannelsWithPeerResponse, CreateInvoiceRequest, CreateInvoiceResponse,
25 GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse, GetRouteHintsRequest,
26 GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse,
27 ListActiveChannelsResponse, OpenChannelResponse, PayInvoiceResponse, PayPrunedInvoiceRequest,
28 SendOnchainResponse,
29};
30use crate::rpc::{CloseChannelsWithPeerPayload, OpenChannelPayload, SendOnchainPayload};
31
32#[derive(Debug)]
37pub struct NetworkLnRpcClient {
38 connection_url: SafeUrl,
39 client: reqwest::Client,
40}
41
42impl NetworkLnRpcClient {
43 pub fn new(url: SafeUrl) -> Self {
44 info!(
45 "Gateway configured to connect to remote LnRpcClient at \n cln extension address: {} ",
46 url.to_string()
47 );
48 NetworkLnRpcClient {
49 connection_url: url,
50 client: reqwest::Client::new(),
51 }
52 }
53
54 async fn call<P: Serialize, T: DeserializeOwned>(
55 &self,
56 method: Method,
57 url: SafeUrl,
58 payload: Option<P>,
59 ) -> Result<T, reqwest::Error> {
60 let mut builder = self.client.request(method, url.clone().to_unsafe());
61 if let Some(payload) = payload {
62 builder = builder
63 .json(&payload)
64 .header(reqwest::header::CONTENT_TYPE, "application/json");
65 }
66
67 let response = builder.send().await?;
68 response.json::<T>().await
69 }
70
71 async fn call_get<T: DeserializeOwned>(&self, url: SafeUrl) -> Result<T, reqwest::Error> {
72 self.call(Method::GET, url, None::<()>).await
73 }
74
75 async fn call_post<P: Serialize, T: DeserializeOwned>(
76 &self,
77 url: SafeUrl,
78 payload: P,
79 ) -> Result<T, reqwest::Error> {
80 self.call(Method::POST, url, Some(payload)).await
81 }
82}
83
84#[async_trait]
85impl ILnRpcClient for NetworkLnRpcClient {
86 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
87 let url = self
88 .connection_url
89 .join(CLN_INFO_ENDPOINT)
90 .expect("invalid base url");
91 self.call_get(url)
92 .await
93 .map_err(|e| LightningRpcError::FailedToGetNodeInfo {
94 failure_reason: e.to_string(),
95 })
96 }
97
98 async fn routehints(
99 &self,
100 num_route_hints: usize,
101 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
102 let url = self
103 .connection_url
104 .join(CLN_ROUTE_HINTS_ENDPOINT)
105 .expect("invalid base url");
106 self.call_post(
107 url,
108 GetRouteHintsRequest {
109 num_route_hints: num_route_hints as u64,
110 },
111 )
112 .await
113 .map_err(|e| LightningRpcError::FailedToGetRouteHints {
114 failure_reason: e.to_string(),
115 })
116 }
117
118 async fn pay_private(
119 &self,
120 invoice: PrunedInvoice,
121 max_delay: u64,
122 max_fee: Amount,
123 ) -> Result<PayInvoiceResponse, LightningRpcError> {
124 let url = self
125 .connection_url
126 .join(CLN_PAY_PRUNED_INVOICE_ENDPOINT)
127 .expect("invalid base url");
128 self.call_post(
129 url,
130 PayPrunedInvoiceRequest {
131 pruned_invoice: Some(invoice),
132 max_delay,
133 max_fee_msat: max_fee,
134 },
135 )
136 .await
137 .map_err(|e| LightningRpcError::FailedPayment {
138 failure_reason: e.to_string(),
139 })
140 }
141
142 fn supports_private_payments(&self) -> bool {
143 true
144 }
145
146 async fn route_htlcs<'a>(
147 self: Box<Self>,
148 _task_group: &TaskGroup,
149 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
150 let url = self
151 .connection_url
152 .join(CLN_ROUTE_HTLCS_ENDPOINT)
153 .expect("invalid base url");
154 let response = reqwest::get(url.to_unsafe()).await.map_err(|e| {
155 LightningRpcError::FailedToRouteHtlcs {
156 failure_reason: e.to_string(),
157 }
158 })?;
159
160 let stream = response.bytes_stream().filter_map(|item| async {
161 match item {
162 Ok(bytes) => {
163 let request = serde_json::from_slice::<InterceptPaymentRequest>(&bytes)
164 .expect("Failed to deserialize InterceptPaymentRequest");
165 Some(request)
166 }
167 Err(e) => {
168 tracing::error!(?e, "Error receiving JSON over stream");
169 None
170 }
171 }
172 });
173
174 Ok((
175 Box::pin(stream),
176 Arc::new(Self::new(self.connection_url.clone())),
177 ))
178 }
179
180 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
181 let url = self
182 .connection_url
183 .join(CLN_COMPLETE_PAYMENT_ENDPOINT)
184 .expect("invalid base url");
185 self.call_post(url, htlc)
186 .await
187 .map_err(|e| LightningRpcError::FailedToCompleteHtlc {
188 failure_reason: e.to_string(),
189 })
190 }
191
192 async fn create_invoice(
193 &self,
194 create_invoice_request: CreateInvoiceRequest,
195 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
196 let url = self
197 .connection_url
198 .join(CLN_CREATE_INVOICE_ENDPOINT)
199 .expect("invalid base url");
200 self.call_post(url, create_invoice_request)
201 .await
202 .map_err(|e| LightningRpcError::FailedToGetInvoice {
203 failure_reason: e.to_string(),
204 })
205 }
206
207 async fn get_ln_onchain_address(
208 &self,
209 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
210 let url = self
211 .connection_url
212 .join(CLN_LN_ONCHAIN_ADDRESS_ENDPOINT)
213 .expect("invalid base url");
214 self.call_get(url)
215 .await
216 .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
217 failure_reason: e.to_string(),
218 })
219 }
220
221 async fn send_onchain(
222 &self,
223 payload: SendOnchainPayload,
224 ) -> Result<SendOnchainResponse, LightningRpcError> {
225 let url = self
226 .connection_url
227 .join(CLN_SEND_ONCHAIN_ENDPOINT)
228 .expect("invalid base url");
229 self.call_post(url, payload)
230 .await
231 .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
232 failure_reason: e.to_string(),
233 })
234 }
235
236 async fn open_channel(
237 &self,
238 payload: OpenChannelPayload,
239 ) -> Result<OpenChannelResponse, LightningRpcError> {
240 let url = self
241 .connection_url
242 .join(CLN_OPEN_CHANNEL_ENDPOINT)
243 .expect("invalid base url");
244 self.call_post(url, payload)
245 .await
246 .map_err(|e| LightningRpcError::FailedToOpenChannel {
247 failure_reason: e.to_string(),
248 })
249 }
250
251 async fn close_channels_with_peer(
252 &self,
253 payload: CloseChannelsWithPeerPayload,
254 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
255 let url = self
256 .connection_url
257 .join(CLN_CLOSE_CHANNELS_WITH_PEER_ENDPOINT)
258 .expect("invalid base url");
259 self.call_post(url, payload).await.map_err(|e| {
260 LightningRpcError::FailedToCloseChannelsWithPeer {
261 failure_reason: e.to_string(),
262 }
263 })
264 }
265
266 async fn list_active_channels(&self) -> Result<Vec<ChannelInfo>, LightningRpcError> {
267 let url = self
268 .connection_url
269 .join(CLN_LIST_ACTIVE_CHANNELS_ENDPOINT)
270 .expect("invalid base url");
271 let response: ListActiveChannelsResponse = self.call_get(url).await.map_err(|e| {
272 LightningRpcError::FailedToListActiveChannels {
273 failure_reason: e.to_string(),
274 }
275 })?;
276 Ok(response.channels)
277 }
278
279 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
280 let url = self
281 .connection_url
282 .join(CLN_GET_BALANCES_ENDPOINT)
283 .expect("invalid base url");
284 self.call_get(url)
285 .await
286 .map_err(|e| LightningRpcError::FailedToGetBalances {
287 failure_reason: e.to_string(),
288 })
289 }
290}