1use std::collections::HashSet;
3use std::sync::{Arc, RwLock as StdRwLock};
4
5use async_trait::async_trait;
6use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
7#[cfg(feature = "auth")]
8use cdk_common::{Method, ProtectedEndpoint, RoutePath};
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11#[cfg(feature = "auth")]
12use tokio::sync::RwLock;
13use tracing::instrument;
14use url::Url;
15use web_time::{Duration, Instant};
16
17use super::transport::Transport;
18use super::{Error, MintConnector};
19use crate::mint_url::MintUrl;
20#[cfg(feature = "auth")]
21use crate::nuts::nut22::MintAuthRequest;
22use crate::nuts::{
23 AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
24 MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
25 MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
26 SwapRequest, SwapResponse,
27};
28#[cfg(feature = "auth")]
29use crate::wallet::auth::{AuthMintConnector, AuthWallet};
30
31type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
32
33#[derive(Debug, Clone)]
35pub struct HttpClient<T>
36where
37 T: Transport + Send + Sync + 'static,
38{
39 transport: Arc<T>,
40 mint_url: MintUrl,
41 cache_support: Arc<StdRwLock<Cache>>,
42 #[cfg(feature = "auth")]
43 auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
44}
45
46impl<T> HttpClient<T>
47where
48 T: Transport + Send + Sync + 'static,
49{
50 #[cfg(feature = "auth")]
52 pub fn with_transport(
53 mint_url: MintUrl,
54 transport: T,
55 auth_wallet: Option<AuthWallet>,
56 ) -> Self {
57 Self {
58 transport: transport.into(),
59 mint_url,
60 auth_wallet: Arc::new(RwLock::new(auth_wallet)),
61 cache_support: Default::default(),
62 }
63 }
64
65 #[cfg(not(feature = "auth"))]
67 pub fn with_transport(mint_url: MintUrl, transport: T) -> Self {
68 Self {
69 transport: transport.into(),
70 mint_url,
71 cache_support: Default::default(),
72 }
73 }
74
75 #[cfg(feature = "auth")]
77 pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
78 Self {
79 transport: T::default().into(),
80 mint_url,
81 auth_wallet: Arc::new(RwLock::new(auth_wallet)),
82 cache_support: Default::default(),
83 }
84 }
85
86 #[cfg(not(feature = "auth"))]
87 pub fn new(mint_url: MintUrl) -> Self {
89 Self {
90 transport: T::default().into(),
91 cache_support: Default::default(),
92 mint_url,
93 }
94 }
95
96 #[cfg(feature = "auth")]
98 #[instrument(skip(self))]
99 pub async fn get_auth_token(
100 &self,
101 method: Method,
102 path: RoutePath,
103 ) -> Result<Option<AuthToken>, Error> {
104 let auth_wallet = self.auth_wallet.read().await;
105 match auth_wallet.as_ref() {
106 Some(auth_wallet) => {
107 let endpoint = ProtectedEndpoint::new(method, path);
108 auth_wallet.get_auth_for_request(&endpoint).await
109 }
110 None => Ok(None),
111 }
112 }
113
114 pub fn with_proxy(
118 mint_url: MintUrl,
119 proxy: Url,
120 host_matcher: Option<&str>,
121 accept_invalid_certs: bool,
122 ) -> Result<Self, Error> {
123 let mut transport = T::default();
124 transport.with_proxy(proxy, host_matcher, accept_invalid_certs)?;
125
126 Ok(Self {
127 transport: transport.into(),
128 mint_url,
129 #[cfg(feature = "auth")]
130 auth_wallet: Arc::new(RwLock::new(None)),
131 cache_support: Default::default(),
132 })
133 }
134
135 #[inline(always)]
139 async fn retriable_http_request<P, R>(
140 &self,
141 method: nut19::Method,
142 path: nut19::Path,
143 auth_token: Option<AuthToken>,
144 payload: &P,
145 ) -> Result<R, Error>
146 where
147 P: Serialize + ?Sized + Send + Sync,
148 R: DeserializeOwned,
149 {
150 let started = Instant::now();
151
152 let retriable_window = self
153 .cache_support
154 .read()
155 .map(|cache_support| {
156 cache_support
157 .1
158 .get(&(method, path))
159 .map(|_| cache_support.0)
160 })
161 .unwrap_or_default()
162 .map(Duration::from_secs)
163 .unwrap_or_default();
164
165 let transport = self.transport.clone();
166 loop {
167 let url = self.mint_url.join_paths(&match path {
168 nut19::Path::MintBolt11 => vec!["v1", "mint", "bolt11"],
169 nut19::Path::MeltBolt11 => vec!["v1", "melt", "bolt11"],
170 nut19::Path::MintBolt12 => vec!["v1", "mint", "bolt12"],
171
172 nut19::Path::MeltBolt12 => vec!["v1", "melt", "bolt12"],
173 nut19::Path::Swap => vec!["v1", "swap"],
174 })?;
175
176 let result = match method {
177 nut19::Method::Get => transport.http_get(url, auth_token.clone()).await,
178 nut19::Method::Post => transport.http_post(url, auth_token.clone(), payload).await,
179 };
180
181 if result.is_ok() {
182 return result;
183 }
184
185 match result.as_ref() {
186 Err(Error::HttpError(status_code, _)) => {
187 let status_code = status_code.to_owned().unwrap_or_default();
188 if (400..=499).contains(&status_code) {
189 return result;
191 }
192
193 tracing::error!("Failed http_request {:?}", result.as_ref().err());
195
196 if retriable_window < started.elapsed() {
197 return result;
198 }
199 }
200 Err(_) => return result,
201 _ => unreachable!(),
202 };
203 }
204 }
205}
206
207#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
208#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
209impl<T> MintConnector for HttpClient<T>
210where
211 T: Transport + Send + Sync + 'static,
212{
213 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
214 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
215 async fn resolve_dns_txt(&self, domain: &str) -> Result<Vec<String>, Error> {
216 self.transport.resolve_dns_txt(domain).await
217 }
218
219 #[instrument(skip(self))]
221 async fn fetch_lnurl_pay_request(
222 &self,
223 url: &str,
224 ) -> Result<crate::lightning_address::LnurlPayResponse, Error> {
225 let parsed_url =
226 url::Url::parse(url).map_err(|e| Error::Custom(format!("Invalid URL: {}", e)))?;
227 self.transport.http_get(parsed_url, None).await
228 }
229
230 #[instrument(skip(self))]
232 async fn fetch_lnurl_invoice(
233 &self,
234 url: &str,
235 ) -> Result<crate::lightning_address::LnurlPayInvoiceResponse, Error> {
236 let parsed_url =
237 url::Url::parse(url).map_err(|e| Error::Custom(format!("Invalid URL: {}", e)))?;
238 self.transport.http_get(parsed_url, None).await
239 }
240
241 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
243 async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
244 let url = self.mint_url.join_paths(&["v1", "keys"])?;
245 let transport = self.transport.clone();
246
247 Ok(transport.http_get::<KeysResponse>(url, None).await?.keysets)
248 }
249
250 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
252 async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
253 let url = self
254 .mint_url
255 .join_paths(&["v1", "keys", &keyset_id.to_string()])?;
256
257 let transport = self.transport.clone();
258 let keys_response = transport.http_get::<KeysResponse>(url, None).await?;
259
260 Ok(keys_response.keysets.first().unwrap().clone())
261 }
262
263 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
265 async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
266 let url = self.mint_url.join_paths(&["v1", "keysets"])?;
267 let transport = self.transport.clone();
268 transport.http_get(url, None).await
269 }
270
271 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
273 async fn post_mint_quote(
274 &self,
275 request: MintQuoteBolt11Request,
276 ) -> Result<MintQuoteBolt11Response<String>, Error> {
277 let url = self
278 .mint_url
279 .join_paths(&["v1", "mint", "quote", "bolt11"])?;
280
281 #[cfg(feature = "auth")]
282 let auth_token = self
283 .get_auth_token(Method::Post, RoutePath::MintQuoteBolt11)
284 .await?;
285
286 #[cfg(not(feature = "auth"))]
287 let auth_token = None;
288
289 self.transport.http_post(url, auth_token, &request).await
290 }
291
292 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
294 async fn get_mint_quote_status(
295 &self,
296 quote_id: &str,
297 ) -> Result<MintQuoteBolt11Response<String>, Error> {
298 let url = self
299 .mint_url
300 .join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?;
301
302 #[cfg(feature = "auth")]
303 let auth_token = self
304 .get_auth_token(Method::Get, RoutePath::MintQuoteBolt11)
305 .await?;
306
307 #[cfg(not(feature = "auth"))]
308 let auth_token = None;
309 self.transport.http_get(url, auth_token).await
310 }
311
312 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
314 async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
315 #[cfg(feature = "auth")]
316 let auth_token = self
317 .get_auth_token(Method::Post, RoutePath::MintBolt11)
318 .await?;
319
320 #[cfg(not(feature = "auth"))]
321 let auth_token = None;
322 self.retriable_http_request(
323 nut19::Method::Post,
324 nut19::Path::MintBolt11,
325 auth_token,
326 &request,
327 )
328 .await
329 }
330
331 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
333 async fn post_melt_quote(
334 &self,
335 request: MeltQuoteBolt11Request,
336 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
337 let url = self
338 .mint_url
339 .join_paths(&["v1", "melt", "quote", "bolt11"])?;
340 #[cfg(feature = "auth")]
341 let auth_token = self
342 .get_auth_token(Method::Post, RoutePath::MeltQuoteBolt11)
343 .await?;
344
345 #[cfg(not(feature = "auth"))]
346 let auth_token = None;
347 self.transport.http_post(url, auth_token, &request).await
348 }
349
350 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
352 async fn get_melt_quote_status(
353 &self,
354 quote_id: &str,
355 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
356 let url = self
357 .mint_url
358 .join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?;
359
360 #[cfg(feature = "auth")]
361 let auth_token = self
362 .get_auth_token(Method::Get, RoutePath::MeltQuoteBolt11)
363 .await?;
364
365 #[cfg(not(feature = "auth"))]
366 let auth_token = None;
367 self.transport.http_get(url, auth_token).await
368 }
369
370 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
373 async fn post_melt(
374 &self,
375 request: MeltRequest<String>,
376 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
377 #[cfg(feature = "auth")]
378 let auth_token = self
379 .get_auth_token(Method::Post, RoutePath::MeltBolt11)
380 .await?;
381
382 #[cfg(not(feature = "auth"))]
383 let auth_token = None;
384
385 self.retriable_http_request(
386 nut19::Method::Post,
387 nut19::Path::MeltBolt11,
388 auth_token,
389 &request,
390 )
391 .await
392 }
393
394 #[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))]
396 async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
397 #[cfg(feature = "auth")]
398 let auth_token = self.get_auth_token(Method::Post, RoutePath::Swap).await?;
399
400 #[cfg(not(feature = "auth"))]
401 let auth_token = None;
402
403 self.retriable_http_request(
404 nut19::Method::Post,
405 nut19::Path::Swap,
406 auth_token,
407 &swap_request,
408 )
409 .await
410 }
411
412 async fn get_mint_info(&self) -> Result<MintInfo, Error> {
414 let url = self.mint_url.join_paths(&["v1", "info"])?;
415 let transport = self.transport.clone();
416 let info: MintInfo = transport.http_get(url, None).await?;
417
418 if let Ok(mut cache_support) = self.cache_support.write() {
419 *cache_support = (
420 info.nuts.nut19.ttl.unwrap_or(300),
421 info.nuts
422 .nut19
423 .cached_endpoints
424 .clone()
425 .into_iter()
426 .map(|cached_endpoint| (cached_endpoint.method, cached_endpoint.path))
427 .collect(),
428 );
429 }
430
431 Ok(info)
432 }
433
434 #[cfg(feature = "auth")]
435 async fn get_auth_wallet(&self) -> Option<AuthWallet> {
436 self.auth_wallet.read().await.clone()
437 }
438
439 #[cfg(feature = "auth")]
440 async fn set_auth_wallet(&self, wallet: Option<AuthWallet>) {
441 *self.auth_wallet.write().await = wallet;
442 }
443
444 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
446 async fn post_check_state(
447 &self,
448 request: CheckStateRequest,
449 ) -> Result<CheckStateResponse, Error> {
450 let url = self.mint_url.join_paths(&["v1", "checkstate"])?;
451 #[cfg(feature = "auth")]
452 let auth_token = self
453 .get_auth_token(Method::Post, RoutePath::Checkstate)
454 .await?;
455
456 #[cfg(not(feature = "auth"))]
457 let auth_token = None;
458 self.transport.http_post(url, auth_token, &request).await
459 }
460
461 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
463 async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
464 let url = self.mint_url.join_paths(&["v1", "restore"])?;
465 #[cfg(feature = "auth")]
466 let auth_token = self
467 .get_auth_token(Method::Post, RoutePath::Restore)
468 .await?;
469
470 #[cfg(not(feature = "auth"))]
471 let auth_token = None;
472 self.transport.http_post(url, auth_token, &request).await
473 }
474
475 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
477 async fn post_mint_bolt12_quote(
478 &self,
479 request: MintQuoteBolt12Request,
480 ) -> Result<MintQuoteBolt12Response<String>, Error> {
481 let url = self
482 .mint_url
483 .join_paths(&["v1", "mint", "quote", "bolt12"])?;
484
485 #[cfg(feature = "auth")]
486 let auth_token = self
487 .get_auth_token(Method::Post, RoutePath::MintQuoteBolt12)
488 .await?;
489
490 #[cfg(not(feature = "auth"))]
491 let auth_token = None;
492
493 self.transport.http_post(url, auth_token, &request).await
494 }
495
496 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
498 async fn get_mint_quote_bolt12_status(
499 &self,
500 quote_id: &str,
501 ) -> Result<MintQuoteBolt12Response<String>, Error> {
502 let url = self
503 .mint_url
504 .join_paths(&["v1", "mint", "quote", "bolt12", quote_id])?;
505
506 #[cfg(feature = "auth")]
507 let auth_token = self
508 .get_auth_token(Method::Get, RoutePath::MintQuoteBolt12)
509 .await?;
510
511 #[cfg(not(feature = "auth"))]
512 let auth_token = None;
513 self.transport.http_get(url, auth_token).await
514 }
515
516 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
518 async fn post_melt_bolt12_quote(
519 &self,
520 request: MeltQuoteBolt12Request,
521 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
522 let url = self
523 .mint_url
524 .join_paths(&["v1", "melt", "quote", "bolt12"])?;
525 #[cfg(feature = "auth")]
526 let auth_token = self
527 .get_auth_token(Method::Post, RoutePath::MeltQuoteBolt12)
528 .await?;
529
530 #[cfg(not(feature = "auth"))]
531 let auth_token = None;
532 self.transport.http_post(url, auth_token, &request).await
533 }
534
535 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
537 async fn get_melt_bolt12_quote_status(
538 &self,
539 quote_id: &str,
540 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
541 let url = self
542 .mint_url
543 .join_paths(&["v1", "melt", "quote", "bolt12", quote_id])?;
544
545 #[cfg(feature = "auth")]
546 let auth_token = self
547 .get_auth_token(Method::Get, RoutePath::MeltQuoteBolt12)
548 .await?;
549
550 #[cfg(not(feature = "auth"))]
551 let auth_token = None;
552 self.transport.http_get(url, auth_token).await
553 }
554
555 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
557 async fn post_melt_bolt12(
558 &self,
559 request: MeltRequest<String>,
560 ) -> Result<MeltQuoteBolt11Response<String>, Error> {
561 #[cfg(feature = "auth")]
562 let auth_token = self
563 .get_auth_token(Method::Post, RoutePath::MeltBolt12)
564 .await?;
565
566 #[cfg(not(feature = "auth"))]
567 let auth_token = None;
568 self.retriable_http_request(
569 nut19::Method::Post,
570 nut19::Path::MeltBolt12,
571 auth_token,
572 &request,
573 )
574 .await
575 }
576}
577
578#[derive(Debug, Clone)]
580#[cfg(feature = "auth")]
581pub struct AuthHttpClient<T>
582where
583 T: Transport + Send + Sync + 'static,
584{
585 transport: Arc<T>,
586 mint_url: MintUrl,
587 cat: Arc<RwLock<AuthToken>>,
588}
589
590#[cfg(feature = "auth")]
591impl<T> AuthHttpClient<T>
592where
593 T: Transport + Send + Sync + 'static,
594{
595 pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
597 Self {
598 transport: T::default().into(),
599 mint_url,
600 cat: Arc::new(RwLock::new(
601 cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
602 )),
603 }
604 }
605}
606
607#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
608#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
609#[cfg(feature = "auth")]
610impl<T> AuthMintConnector for AuthHttpClient<T>
611where
612 T: Transport + Send + Sync + 'static,
613{
614 async fn get_auth_token(&self) -> Result<AuthToken, Error> {
615 Ok(self.cat.read().await.clone())
616 }
617
618 async fn set_auth_token(&self, token: AuthToken) -> Result<(), Error> {
619 *self.cat.write().await = token;
620 Ok(())
621 }
622
623 async fn get_mint_info(&self) -> Result<MintInfo, Error> {
625 let url = self.mint_url.join_paths(&["v1", "info"])?;
626 let mint_info: MintInfo = self.transport.http_get::<MintInfo>(url, None).await?;
627
628 Ok(mint_info)
629 }
630
631 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
633 async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
634 let url =
635 self.mint_url
636 .join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
637
638 let mut keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
639
640 let keyset = keys_response
641 .keysets
642 .drain(0..1)
643 .next()
644 .ok_or_else(|| Error::UnknownKeySet)?;
645
646 Ok(keyset)
647 }
648
649 #[instrument(skip(self), fields(mint_url = %self.mint_url))]
651 async fn get_mint_blind_auth_keysets(&self) -> Result<KeysetResponse, Error> {
652 let url = self
653 .mint_url
654 .join_paths(&["v1", "auth", "blind", "keysets"])?;
655
656 self.transport.http_get(url, None).await
657 }
658
659 #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
661 async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
662 let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
663 self.transport
664 .http_post(url, Some(self.cat.read().await.clone()), &request)
665 .await
666 }
667}