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#[derive(Debug, Clone, Default)]
29pub struct Builder {
30 pub proxy: Option<String>,
41 pub timeout: Option<u64>,
43}
44
45impl Builder {
46 pub fn proxy(mut self, proxy: &str) -> Self {
48 self.proxy = Some(proxy.to_string());
49 self
50 }
51
52 pub fn timeout(mut self, timeout: u64) -> Self {
54 self.timeout = Some(timeout);
55 self
56 }
57
58 #[cfg(feature = "blocking")]
60 pub fn build_blocking(self) -> Result<BlockingClient, Error> {
61 BlockingClient::from_builder(self)
62 }
63
64 #[cfg(feature = "async")]
66 pub fn build_async(self) -> Result<AsyncClient, Error> {
67 AsyncClient::from_builder(self)
68 }
69}
70
71#[derive(Debug)]
73pub enum Error {
74 InvalidLnUrl,
76 InvalidLightningAddress,
78 InvalidComment,
80 InvalidAmount,
82 #[cfg(feature = "blocking")]
84 Ureq(ureq::Error),
85 #[cfg(any(feature = "async", feature = "async-https"))]
87 Reqwest(reqwest::Error),
88 HttpResponse(u16),
90 Io(io::Error),
92 Json(serde_json::Error),
94 InvalidResponse,
96 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 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 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 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 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 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 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}