ln_gateway/lightning/
cln.rs

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/// An `ILnRpcClient` that wraps around `GatewayLightningClient` for
33/// convenience, and makes real RPC requests over the wire to a remote lightning
34/// node. The lightning node is exposed via a corresponding
35/// `GatewayLightningServer`.
36#[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}