Skip to main content

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    /// The BOLT11 invoice returned by the callback could not be parsed
83    InvalidInvoice(String),
84    /// The BOLT11 invoice amount does not match the requested amount
85    InvoiceAmountMismatch {
86        /// The amount that was requested, in millisatoshis
87        requested_msats: u64,
88        /// The amount encoded in the returned invoice, in millisatoshis,
89        /// or `None` if the invoice did not specify an amount
90        invoice_msats: Option<u64>,
91    },
92    /// Error during ureq HTTP request
93    #[cfg(feature = "blocking")]
94    Ureq(ureq::Error),
95    /// Error during reqwest HTTP request
96    #[cfg(any(feature = "async", feature = "async-https"))]
97    Reqwest(reqwest::Error),
98    /// HTTP response error
99    HttpResponse(u16),
100    /// IO error during ureq response read
101    Io(io::Error),
102    /// Error decoding JSON
103    Json(serde_json::Error),
104    /// Invalid Response
105    InvalidResponse,
106    /// Other error
107    Other(String),
108}
109
110impl fmt::Display for Error {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{:?}", self)
113    }
114}
115
116macro_rules! impl_error {
117    ( $from:ty, $to:ident ) => {
118        impl_error!($from, $to, Error);
119    };
120    ( $from:ty, $to:ident, $impl_for:ty ) => {
121        impl std::convert::From<$from> for $impl_for {
122            fn from(err: $from) -> Self {
123                <$impl_for>::$to(err)
124            }
125        }
126    };
127}
128
129impl std::error::Error for Error {}
130#[cfg(any(feature = "async", feature = "async-https"))]
131impl_error!(::reqwest::Error, Reqwest, Error);
132impl_error!(io::Error, Io, Error);
133impl_error!(serde_json::Error, Json, Error);
134
135#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
136#[cfg(test)]
137mod tests {
138    use crate::lightning_address::LightningAddress;
139    use crate::LnUrlResponse::LnUrlPayResponse;
140    use crate::{AsyncClient, BlockingClient, Builder};
141    use lightning_invoice::Bolt11Invoice;
142    use nostr::prelude::ZapRequestData;
143    use nostr::{EventBuilder, JsonUtil, Keys};
144    use std::str::FromStr;
145
146    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
147    async fn setup_clients() -> (BlockingClient, AsyncClient) {
148        let blocking_client = Builder::default().build_blocking().unwrap();
149        let async_client = Builder::default().build_async().unwrap();
150
151        (blocking_client, async_client)
152    }
153
154    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
155    #[tokio::test]
156    async fn test_get_invoice() {
157        let url = "https://benthecarman.com/.well-known/lnurlp/ben";
158        let (blocking_client, async_client) = setup_clients().await;
159
160        let res = blocking_client.make_request(url).unwrap();
161        let res_async = async_client.make_request(url).await.unwrap();
162
163        // check res_async
164        match res_async {
165            LnUrlPayResponse(_) => {}
166            _ => panic!("Wrong response type"),
167        }
168
169        if let LnUrlPayResponse(pay) = res {
170            let msats = 1_000_000;
171            let invoice = blocking_client
172                .get_invoice(&pay, msats, None, None)
173                .unwrap();
174            let invoice_async = async_client
175                .get_invoice(&pay, msats, None, None)
176                .await
177                .unwrap();
178
179            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
180            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
181
182            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
183            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
184        } else {
185            panic!("Wrong response type");
186        }
187    }
188
189    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
190    #[tokio::test]
191    async fn test_get_zap_invoice() {
192        let url = "https://benthecarman.com/.well-known/lnurlp/ben";
193        let (blocking_client, async_client) = setup_clients().await;
194
195        let res = blocking_client.make_request(url).unwrap();
196        let res_async = async_client.make_request(url).await.unwrap();
197
198        // check res_async
199        match res_async {
200            LnUrlPayResponse(_) => {}
201            _ => panic!("Wrong response type"),
202        }
203
204        if let LnUrlPayResponse(pay) = res {
205            let msats = 1_000_000;
206
207            let keys = Keys::generate();
208            let event = {
209                let data = ZapRequestData {
210                    public_key: keys.public_key(),
211                    relays: vec![],
212                    amount: Some(msats),
213                    lnurl: None,
214                    event_id: None,
215                    event_coordinate: None,
216                };
217                EventBuilder::new_zap_request(data).to_event(&keys).unwrap()
218            };
219
220            let invoice = blocking_client
221                .get_invoice(&pay, msats, Some(event.as_json()), None)
222                .unwrap();
223            let invoice_async = async_client
224                .get_invoice(&pay, msats, Some(event.as_json()), None)
225                .await
226                .unwrap();
227
228            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
229            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
230
231            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
232            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
233        } else {
234            panic!("Wrong response type");
235        }
236    }
237
238    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
239    #[tokio::test]
240    async fn test_get_invoice_with_comment() {
241        let url = "https://primal.net/.well-known/lnurlp/odell";
242        let (blocking_client, async_client) = setup_clients().await;
243
244        let res = blocking_client.make_request(url).unwrap();
245        let res_async = async_client.make_request(url).await.unwrap();
246
247        // check res_async
248        match res_async {
249            LnUrlPayResponse(_) => {}
250            _ => panic!("Wrong response type"),
251        }
252
253        if let LnUrlPayResponse(pay) = res {
254            let msats = 1_000_000;
255
256            let comment = "test comment".to_string();
257
258            let invoice = blocking_client
259                .get_invoice(&pay, msats, None, Some(&comment))
260                .unwrap();
261            let invoice_async = async_client
262                .get_invoice(&pay, msats, None, Some(&comment))
263                .await
264                .unwrap();
265
266            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
267            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
268
269            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
270            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
271        } else {
272            panic!("Wrong response type");
273        }
274    }
275
276    #[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
277    #[tokio::test]
278    async fn test_get_invoice_ln_addr() {
279        let ln_addr = LightningAddress::from_str("jack@cash.app").unwrap();
280        let (blocking_client, async_client) = setup_clients().await;
281
282        let res = blocking_client
283            .make_request(ln_addr.lnurlp_url().as_str())
284            .unwrap();
285        let res_async = async_client
286            .make_request(ln_addr.lnurlp_url().as_str())
287            .await
288            .unwrap();
289
290        // check res_async
291        match res_async {
292            LnUrlPayResponse(_) => {}
293            _ => panic!("Wrong response type"),
294        }
295
296        if let LnUrlPayResponse(pay) = res {
297            let msats = 1_000_000;
298            let invoice = blocking_client
299                .get_invoice(&pay, msats, None, None)
300                .unwrap();
301            let invoice_async = async_client
302                .get_invoice(&pay, msats, None, None)
303                .await
304                .unwrap();
305
306            let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
307            let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
308
309            assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
310            assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
311        } else {
312            panic!("Wrong response type");
313        }
314    }
315}