lnurl/
lib.rs

1#![allow(clippy::large_enum_variant)]
2#![allow(clippy::result_large_err)]
3
4pub mod api;
5mod auth;
6pub mod channel;
7pub mod lightning_address;
8pub mod lnurl;
9pub mod pay;
10pub mod withdraw;
11
12#[cfg(any(feature = "async", feature = "async-https"))]
13pub mod r#async;
14#[cfg(feature = "blocking")]
15pub mod blocking;
16
17pub use auth::get_derivation_path;
18
19pub use api::*;
20#[cfg(feature = "blocking")]
21pub use blocking::BlockingClient;
22#[cfg(any(feature = "async", feature = "async-https"))]
23pub use r#async::AsyncClient;
24use std::{fmt, io};
25
26// All this copy-pasted from rust-esplora-client
27
28#[derive(Debug, Clone, Default)]
29pub struct Builder {
30    /// Optional URL of the proxy to use to make requests to the LNURL server
31    ///
32    /// The string should be formatted as: `<protocol>://<user>:<password>@host:<port>`.
33    ///
34    /// Note that the format of this value and the supported protocols change slightly between the
35    /// blocking version of the client (using `ureq`) and the async version (using `reqwest`). For more
36    /// details check with the documentation of the two crates. Both of them are compiled with
37    /// the `socks` feature enabled.
38    ///
39    /// The proxy is ignored when targeting `wasm32`.
40    pub proxy: Option<String>,
41    /// Socket timeout.
42    pub timeout: Option<u64>,
43}
44
45impl Builder {
46    /// Set the proxy of the builder
47    pub fn proxy(mut self, proxy: &str) -> Self {
48        self.proxy = Some(proxy.to_string());
49        self
50    }
51
52    /// Set the timeout of the builder
53    pub fn timeout(mut self, timeout: u64) -> Self {
54        self.timeout = Some(timeout);
55        self
56    }
57
58    /// build a blocking client from builder
59    #[cfg(feature = "blocking")]
60    pub fn build_blocking(self) -> Result<BlockingClient, Error> {
61        BlockingClient::from_builder(self)
62    }
63
64    /// build an asynchronous client from builder
65    #[cfg(feature = "async")]
66    pub fn build_async(self) -> Result<AsyncClient, Error> {
67        AsyncClient::from_builder(self)
68    }
69}
70
71/// Errors that can happen during a sync with a LNURL service
72#[derive(Debug)]
73pub enum Error {
74    /// Error decoding lnurl
75    InvalidLnUrl,
76    /// Error decoding lightning address
77    InvalidLightningAddress,
78    /// Invalid LnURL pay comment
79    InvalidComment,
80    /// Invalid amount on request
81    InvalidAmount,
82    /// Error during ureq HTTP request
83    #[cfg(feature = "blocking")]
84    Ureq(ureq::Error),
85    /// Error during reqwest HTTP request
86    #[cfg(any(feature = "async", feature = "async-https"))]
87    Reqwest(reqwest::Error),
88    /// HTTP response error
89    HttpResponse(u16),
90    /// IO error during ureq response read
91    Io(io::Error),
92    /// Error decoding JSON
93    Json(serde_json::Error),
94    /// Invalid Response
95    InvalidResponse,
96    /// Other error
97    Other(String),
98}
99
100impl fmt::Display for Error {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{:?}", self)
103    }
104}
105
106macro_rules! impl_error {
107    ( $from:ty, $to:ident ) => {
108        impl_error!($from, $to, Error);
109    };
110    ( $from:ty, $to:ident, $impl_for:ty ) => {
111        impl std::convert::From<$from> for $impl_for {
112            fn from(err: $from) -> Self {
113                <$impl_for>::$to(err)
114            }
115        }
116    };
117}
118
119impl std::error::Error for Error {}
120#[cfg(any(feature = "async", feature = "async-https"))]
121impl_error!(::reqwest::Error, Reqwest, Error);
122impl_error!(io::Error, Io, Error);
123impl_error!(serde_json::Error, Json, Error);
124
125#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
126#[cfg(test)]
127mod tests {
128    use crate::lightning_address::LightningAddress;
129    use crate::lnurl::LnUrl;
130    use crate::LnUrlResponse::{LnUrlChannelResponse, LnUrlPayResponse, LnUrlWithdrawResponse};
131    use crate::{AsyncClient, BlockingClient, Builder, Response};
132    use bitcoin::secp256k1::PublicKey;
133    use lightning_invoice::Bolt11Invoice;
134    use nostr::prelude::ZapRequestData;
135    use nostr::{EventBuilder, JsonUtil, Keys};
136    use std::str::FromStr;
137
138    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
139    async fn setup_clients() -> (BlockingClient, AsyncClient) {
140        let blocking_client = Builder::default().build_blocking().unwrap();
141        let async_client = Builder::default().build_async().unwrap();
142
143        (blocking_client, async_client)
144    }
145
146    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
147    #[tokio::test]
148    async fn test_get_invoice() {
149        let url = "https://benthecarman.com/.well-known/lnurlp/ben";
150        let (blocking_client, async_client) = setup_clients().await;
151
152        let res = blocking_client.make_request(url).unwrap();
153        let res_async = async_client.make_request(url).await.unwrap();
154
155        // check res_async
156        match res_async {
157            LnUrlPayResponse(_) => {}
158            _ => panic!("Wrong response type"),
159        }
160
161        if let LnUrlPayResponse(pay) = res {
162            let msats = 1_000_000;
163            let invoice = blocking_client
164                .get_invoice(&pay, msats, None, None)
165                .unwrap();
166            let invoice_async = async_client
167                .get_invoice(&pay, msats, None, None)
168                .await
169                .unwrap();
170
171            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
172            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
173
174            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
175            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
176        } else {
177            panic!("Wrong response type");
178        }
179    }
180
181    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
182    #[tokio::test]
183    async fn test_get_zap_invoice() {
184        let url = "https://benthecarman.com/.well-known/lnurlp/ben";
185        let (blocking_client, async_client) = setup_clients().await;
186
187        let res = blocking_client.make_request(url).unwrap();
188        let res_async = async_client.make_request(url).await.unwrap();
189
190        // check res_async
191        match res_async {
192            LnUrlPayResponse(_) => {}
193            _ => panic!("Wrong response type"),
194        }
195
196        if let LnUrlPayResponse(pay) = res {
197            let msats = 1_000_000;
198
199            let keys = Keys::generate();
200            let event = {
201                let data = ZapRequestData {
202                    public_key: keys.public_key(),
203                    relays: vec![],
204                    amount: Some(msats),
205                    lnurl: None,
206                    event_id: None,
207                    event_coordinate: None,
208                };
209                EventBuilder::new_zap_request(data).to_event(&keys).unwrap()
210            };
211
212            let invoice = blocking_client
213                .get_invoice(&pay, msats, Some(event.as_json()), None)
214                .unwrap();
215            let invoice_async = async_client
216                .get_invoice(&pay, msats, Some(event.as_json()), None)
217                .await
218                .unwrap();
219
220            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
221            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
222
223            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
224            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
225        } else {
226            panic!("Wrong response type");
227        }
228    }
229
230    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
231    #[tokio::test]
232    async fn test_get_invoice_with_comment() {
233        let url = "https://getalby.com/.well-known/lnurlp/nvk";
234        let (blocking_client, async_client) = setup_clients().await;
235
236        let res = blocking_client.make_request(url).unwrap();
237        let res_async = async_client.make_request(url).await.unwrap();
238
239        // check res_async
240        match res_async {
241            LnUrlPayResponse(_) => {}
242            _ => panic!("Wrong response type"),
243        }
244
245        if let LnUrlPayResponse(pay) = res {
246            let msats = 1_000_000;
247
248            let comment = "test comment".to_string();
249
250            let invoice = blocking_client
251                .get_invoice(&pay, msats, None, Some(&comment))
252                .unwrap();
253            let invoice_async = async_client
254                .get_invoice(&pay, msats, None, Some(&comment))
255                .await
256                .unwrap();
257
258            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
259            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
260
261            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
262            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
263        } else {
264            panic!("Wrong response type");
265        }
266    }
267
268    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
269    #[tokio::test]
270    async fn test_get_invoice_ln_addr() {
271        let ln_addr = LightningAddress::from_str("ben@opreturnbot.com").unwrap();
272        let (blocking_client, async_client) = setup_clients().await;
273
274        let res = blocking_client
275            .make_request(ln_addr.lnurlp_url().as_str())
276            .unwrap();
277        let res_async = async_client
278            .make_request(ln_addr.lnurlp_url().as_str())
279            .await
280            .unwrap();
281
282        // check res_async
283        match res_async {
284            LnUrlPayResponse(_) => {}
285            _ => panic!("Wrong response type"),
286        }
287
288        if let LnUrlPayResponse(pay) = res {
289            let msats = 1_000_000;
290            let invoice = blocking_client
291                .get_invoice(&pay, msats, None, None)
292                .unwrap();
293            let invoice_async = async_client
294                .get_invoice(&pay, msats, None, None)
295                .await
296                .unwrap();
297
298            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
299            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
300
301            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
302            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
303        } else {
304            panic!("Wrong response type");
305        }
306    }
307
308    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
309    #[tokio::test]
310    async fn test_do_withdrawal() {
311        let lnurl = LnUrl::from_str("LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHW6T5DPJ8YCTH8AEK2UMND9HKU0FJVSCNZDPHVYENVDTPVYCRSVMPXVMRSCEEXGERQVPSXV6X2C3KX9JXZVMZ8PNXZDR9VY6N2DRZVG6RWEPCVYMRZDMRV9SK2D3KV43XVCF58DT").unwrap();
312        let url = lnurl.url.as_str();
313        let (blocking_client, async_client) = setup_clients().await;
314
315        let res = blocking_client.make_request(url).unwrap();
316        let res_async = async_client.make_request(url).await.unwrap();
317
318        // check res_async
319        match res_async {
320            LnUrlWithdrawResponse(_) => {}
321            _ => panic!("Wrong response type"),
322        }
323
324        if let LnUrlWithdrawResponse(w) = res {
325            let invoice = "lnbc1302470n1p3x3ssapp5axqf6dsusf98895vdhw97rn0szk4z6cxa5hfw3s2q5ksn3575qssdzz2pskjepqw3hjqnmsv4h9xct5wvszsnmjv3jhygzfgsazqem9dejhyctvtan82mny9ycqzpgxqzuysp5q97feeev2tnjsc0qn9kezqlgs8eekwfkxsc28uwxp9elnzkj2n0s9qyyssq02hkrz7dr0adx09t6w2tr9k8nczvq094r7qx297tsdupgeg5t3m8hvmkl7mqhtvx94he3swlg2qzhqk2j39wehcmv9awc06gex82e8qq0u0pm6";
326            let response = blocking_client.do_withdrawal(&w, invoice).unwrap();
327            let response_async = async_client.do_withdrawal(&w, invoice).await.unwrap();
328
329            assert_eq!(response, Response::Ok { event: None });
330            assert_eq!(response_async, Response::Ok { event: None });
331        } else {
332            panic!("Wrong response type");
333        }
334    }
335
336    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
337    #[tokio::test]
338    async fn test_open_channel() {
339        let lnurl = LnUrl::from_str("LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKKX6RPDEHX2MPLWDJHXUMFDAHR6ERR8YCNZEF3XYUNXENRVENXYDF3XQ6XGVEKXGMRQC3CX33N2ERXVC6KZCE38YCNQDF5VDJR2VPEVV6KVC3SV4JRYENX8YUXGEFEX4SSQ7L4MQ").unwrap();
340        let url = lnurl.url.as_str();
341        let (blocking_client, async_client) = setup_clients().await;
342
343        let res = blocking_client.make_request(url).unwrap();
344        let res_async = async_client.make_request(url).await.unwrap();
345
346        // check res_async
347        match res_async {
348            LnUrlChannelResponse(_) => {}
349            _ => panic!("Wrong response type"),
350        }
351
352        if let LnUrlChannelResponse(chan) = res {
353            let node_id = PublicKey::from_str(
354                "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa",
355            )
356            .unwrap();
357            let response = blocking_client.open_channel(&chan, node_id, true).unwrap();
358            let response_async = async_client
359                .open_channel(&chan, node_id, true)
360                .await
361                .unwrap();
362
363            assert_eq!(response, Response::Ok { event: None });
364            assert_eq!(response_async, Response::Ok { event: None });
365        } else {
366            panic!("Wrong response type");
367        }
368    }
369}