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 InvalidInvoice(String),
84 InvoiceAmountMismatch {
86 requested_msats: u64,
88 invoice_msats: Option<u64>,
91 },
92 #[cfg(feature = "blocking")]
94 Ureq(ureq::Error),
95 #[cfg(any(feature = "async", feature = "async-https"))]
97 Reqwest(reqwest::Error),
98 HttpResponse(u16),
100 Io(io::Error),
102 Json(serde_json::Error),
104 InvalidResponse,
106 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 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 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 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 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}