1use anyhow::Result;
2use axum::extract::ws::WebSocketUpgrade;
3use axum::extract::{FromRequestParts, Json, Path, State};
4use axum::http::request::Parts;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use cdk::error::{ErrorCode, ErrorResponse};
8use cdk::mint::QuoteId;
9#[cfg(feature = "auth")]
10use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
11use cdk::nuts::{
12 CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
13 MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
14 MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
15 SwapRequest, SwapResponse,
16};
17use cdk::util::unix_time;
18use paste::paste;
19use tracing::instrument;
20
21#[cfg(feature = "auth")]
22use crate::auth::AuthHeader;
23use crate::ws::main_websocket;
24use crate::MintState;
25
26const PREFER_HEADER_KEY: &str = "Prefer";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct PreferHeader {
34 pub respond_async: bool,
35}
36
37impl<S> FromRequestParts<S> for PreferHeader
38where
39 S: Send + Sync,
40{
41 type Rejection = (StatusCode, String);
42
43 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
44 if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
46 let value = prefer_value.to_str().map_err(|_| {
47 (
48 StatusCode::BAD_REQUEST,
49 "Invalid Prefer header value".to_string(),
50 )
51 })?;
52
53 let respond_async = value.to_lowercase().contains("respond-async");
55
56 return Ok(PreferHeader { respond_async });
57 }
58
59 Ok(PreferHeader {
61 respond_async: false,
62 })
63 }
64}
65
66#[macro_export]
68macro_rules! post_cache_wrapper {
69 ($handler:ident, $request_type:ty, $response_type:ty) => {
70 paste! {
71 pub async fn [<cache_ $handler>](
74 #[cfg(feature = "auth")] auth: AuthHeader,
75 state: State<MintState>,
76 payload: Json<$request_type>
77 ) -> Result<Json<$response_type>, Response> {
78 use std::ops::Deref;
79 let json_extracted_payload = payload.deref();
80 let State(mint_state) = state.clone();
81 let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
82 Some(key) => key,
83 None => {
84 #[cfg(feature = "auth")]
86 return $handler(auth, state, payload).await;
87 #[cfg(not(feature = "auth"))]
88 return $handler( state, payload).await;
89 }
90 };
91 if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
92 return Ok(Json(cached_response));
93 }
94 #[cfg(feature = "auth")]
95 let response = $handler(auth, state, payload).await?;
96 #[cfg(not(feature = "auth"))]
97 let response = $handler(state, payload).await?;
98 mint_state.cache.set(cache_key, &response.deref()).await;
99 Ok(response)
100 }
101 }
102 };
103}
104
105#[macro_export]
107macro_rules! post_cache_wrapper_with_prefer {
108 ($handler:ident, $request_type:ty, $response_type:ty) => {
109 paste! {
110 pub async fn [<cache_ $handler>](
113 #[cfg(feature = "auth")] auth: AuthHeader,
114 prefer: PreferHeader,
115 state: State<MintState>,
116 payload: Json<$request_type>
117 ) -> Result<Json<$response_type>, Response> {
118 use std::ops::Deref;
119
120 let json_extracted_payload = payload.deref();
121 let State(mint_state) = state.clone();
122 let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
123 Some(key) => key,
124 None => {
125 #[cfg(feature = "auth")]
127 return $handler(auth, prefer, state, payload).await;
128 #[cfg(not(feature = "auth"))]
129 return $handler(prefer, state, payload).await;
130 }
131 };
132 if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
133 return Ok(Json(cached_response));
134 }
135 #[cfg(feature = "auth")]
136 let response = $handler(auth, prefer, state, payload).await?;
137 #[cfg(not(feature = "auth"))]
138 let response = $handler(prefer, state, payload).await?;
139 mint_state.cache.set(cache_key, &response.deref()).await;
140 Ok(response)
141 }
142 }
143 };
144}
145
146post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
147post_cache_wrapper!(post_mint_bolt11, MintRequest<QuoteId>, MintResponse);
148post_cache_wrapper_with_prefer!(
149 post_melt_bolt11,
150 MeltRequest<QuoteId>,
151 MeltQuoteBolt11Response<QuoteId>
152);
153
154#[cfg_attr(feature = "swagger", utoipa::path(
155 get,
156 context_path = "/v1",
157 path = "/keys",
158 responses(
159 (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json")
160 )
161))]
162#[instrument(skip_all)]
166pub(crate) async fn get_keys(
167 State(state): State<MintState>,
168) -> Result<Json<KeysResponse>, Response> {
169 Ok(Json(state.mint.pubkeys()))
170}
171
172#[cfg_attr(feature = "swagger", utoipa::path(
173 get,
174 context_path = "/v1",
175 path = "/keys/{keyset_id}",
176 params(
177 ("keyset_id" = String, description = "The keyset ID"),
178 ),
179 responses(
180 (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json"),
181 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
182 )
183))]
184#[instrument(skip_all, fields(keyset_id = ?keyset_id))]
188pub(crate) async fn get_keyset_pubkeys(
189 State(state): State<MintState>,
190 Path(keyset_id): Path<Id>,
191) -> Result<Json<KeysResponse>, Response> {
192 let pubkeys = state.mint.keyset_pubkeys(&keyset_id).map_err(|err| {
193 tracing::error!("Could not get keyset pubkeys: {}", err);
194 into_response(err)
195 })?;
196
197 Ok(Json(pubkeys))
198}
199
200#[cfg_attr(feature = "swagger", utoipa::path(
201 get,
202 context_path = "/v1",
203 path = "/keysets",
204 responses(
205 (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"),
206 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
207 )
208))]
209#[instrument(skip_all)]
213pub(crate) async fn get_keysets(
214 State(state): State<MintState>,
215) -> Result<Json<KeysetResponse>, Response> {
216 Ok(Json(state.mint.keysets()))
217}
218
219#[cfg_attr(feature = "swagger", utoipa::path(
220 post,
221 context_path = "/v1",
222 path = "/mint/quote/bolt11",
223 request_body(content = MintQuoteBolt11Request, description = "Request params", content_type = "application/json"),
224 responses(
225 (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
226 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
227 )
228))]
229#[instrument(skip_all, fields(amount = ?payload.amount))]
233pub(crate) async fn post_mint_bolt11_quote(
234 #[cfg(feature = "auth")] auth: AuthHeader,
235 State(state): State<MintState>,
236 Json(payload): Json<MintQuoteBolt11Request>,
237) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
238 #[cfg(feature = "auth")]
239 state
240 .mint
241 .verify_auth(
242 auth.into(),
243 &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
244 )
245 .await
246 .map_err(into_response)?;
247
248 let quote = state
249 .mint
250 .get_mint_quote(payload.into())
251 .await
252 .map_err(into_response)?;
253
254 Ok(Json(quote.try_into().map_err(into_response)?))
255}
256
257#[cfg_attr(feature = "swagger", utoipa::path(
258 get,
259 context_path = "/v1",
260 path = "/mint/quote/bolt11/{quote_id}",
261 params(
262 ("quote_id" = String, description = "The quote ID"),
263 ),
264 responses(
265 (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
266 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
267 )
268))]
269#[instrument(skip_all, fields(quote_id = ?quote_id))]
273pub(crate) async fn get_check_mint_bolt11_quote(
274 #[cfg(feature = "auth")] auth: AuthHeader,
275 State(state): State<MintState>,
276 Path(quote_id): Path<QuoteId>,
277) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
278 #[cfg(feature = "auth")]
279 {
280 state
281 .mint
282 .verify_auth(
283 auth.into(),
284 &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
285 )
286 .await
287 .map_err(into_response)?;
288 }
289
290 let quote = state
291 .mint
292 .check_mint_quote("e_id)
293 .await
294 .map_err(|err| {
295 tracing::error!("Could not check mint quote {}: {}", quote_id, err);
296 into_response(err)
297 })?;
298
299 Ok(Json(quote.try_into().map_err(into_response)?))
300}
301
302#[instrument(skip_all)]
303pub(crate) async fn ws_handler(
304 #[cfg(feature = "auth")] auth: AuthHeader,
305 State(state): State<MintState>,
306 ws: WebSocketUpgrade,
307) -> Result<impl IntoResponse, Response> {
308 #[cfg(feature = "auth")]
309 {
310 state
311 .mint
312 .verify_auth(
313 auth.into(),
314 &ProtectedEndpoint::new(Method::Get, RoutePath::Ws),
315 )
316 .await
317 .map_err(into_response)?;
318 }
319
320 Ok(ws.on_upgrade(|ws| main_websocket(ws, state)))
321}
322
323#[cfg_attr(feature = "swagger", utoipa::path(
329 post,
330 context_path = "/v1",
331 path = "/mint/bolt11",
332 request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
333 responses(
334 (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
335 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
336 )
337))]
338#[instrument(skip_all, fields(quote_id = ?payload.quote))]
339pub(crate) async fn post_mint_bolt11(
340 #[cfg(feature = "auth")] auth: AuthHeader,
341 State(state): State<MintState>,
342 Json(payload): Json<MintRequest<QuoteId>>,
343) -> Result<Json<MintResponse>, Response> {
344 #[cfg(feature = "auth")]
345 {
346 state
347 .mint
348 .verify_auth(
349 auth.into(),
350 &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
351 )
352 .await
353 .map_err(into_response)?;
354 }
355
356 let res = state
357 .mint
358 .process_mint_request(payload)
359 .await
360 .map_err(|err| {
361 tracing::error!("Could not process mint: {}", err);
362 into_response(err)
363 })?;
364
365 Ok(Json(res))
366}
367
368#[cfg_attr(feature = "swagger", utoipa::path(
369 post,
370 context_path = "/v1",
371 path = "/melt/quote/bolt11",
372 request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"),
373 responses(
374 (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
375 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
376 )
377))]
378#[instrument(skip_all)]
379pub(crate) async fn post_melt_bolt11_quote(
381 #[cfg(feature = "auth")] auth: AuthHeader,
382 State(state): State<MintState>,
383 Json(payload): Json<MeltQuoteBolt11Request>,
384) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
385 #[cfg(feature = "auth")]
386 {
387 state
388 .mint
389 .verify_auth(
390 auth.into(),
391 &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
392 )
393 .await
394 .map_err(into_response)?;
395 }
396
397 let quote = state
398 .mint
399 .get_melt_quote(payload.into())
400 .await
401 .map_err(into_response)?;
402
403 Ok(Json(quote))
404}
405
406#[cfg_attr(feature = "swagger", utoipa::path(
407 get,
408 context_path = "/v1",
409 path = "/melt/quote/bolt11/{quote_id}",
410 params(
411 ("quote_id" = String, description = "The quote ID"),
412 ),
413 responses(
414 (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
415 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
416 )
417))]
418#[instrument(skip_all, fields(quote_id = ?quote_id))]
422pub(crate) async fn get_check_melt_bolt11_quote(
423 #[cfg(feature = "auth")] auth: AuthHeader,
424 State(state): State<MintState>,
425 Path(quote_id): Path<QuoteId>,
426) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
427 #[cfg(feature = "auth")]
428 {
429 state
430 .mint
431 .verify_auth(
432 auth.into(),
433 &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
434 )
435 .await
436 .map_err(into_response)?;
437 }
438
439 let quote = state
440 .mint
441 .check_melt_quote("e_id)
442 .await
443 .map_err(|err| {
444 tracing::error!("Could not check melt quote: {}", err);
445 into_response(err)
446 })?;
447
448 Ok(Json(quote))
449}
450
451#[cfg_attr(feature = "swagger", utoipa::path(
452 post,
453 context_path = "/v1",
454 path = "/melt/bolt11",
455 request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
456 responses(
457 (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
458 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
459 )
460))]
461#[instrument(skip_all)]
465pub(crate) async fn post_melt_bolt11(
466 #[cfg(feature = "auth")] auth: AuthHeader,
467 prefer: PreferHeader,
468 State(state): State<MintState>,
469 Json(payload): Json<MeltRequest<QuoteId>>,
470) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
471 #[cfg(feature = "auth")]
472 {
473 state
474 .mint
475 .verify_auth(
476 auth.into(),
477 &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
478 )
479 .await
480 .map_err(into_response)?;
481 }
482
483 let res = if prefer.respond_async {
484 state
486 .mint
487 .melt_async(&payload)
488 .await
489 .map_err(into_response)?
490 } else {
491 state.mint.melt(&payload).await.map_err(into_response)?
493 };
494
495 Ok(Json(res))
496}
497
498#[cfg_attr(feature = "swagger", utoipa::path(
499 post,
500 context_path = "/v1",
501 path = "/checkstate",
502 request_body(content = CheckStateRequest, description = "State params", content_type = "application/json"),
503 responses(
504 (status = 200, description = "Successful response", body = CheckStateResponse, content_type = "application/json"),
505 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
506 )
507))]
508#[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
512pub(crate) async fn post_check(
513 #[cfg(feature = "auth")] auth: AuthHeader,
514 State(state): State<MintState>,
515 Json(payload): Json<CheckStateRequest>,
516) -> Result<Json<CheckStateResponse>, Response> {
517 #[cfg(feature = "auth")]
518 {
519 state
520 .mint
521 .verify_auth(
522 auth.into(),
523 &ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
524 )
525 .await
526 .map_err(into_response)?;
527 }
528
529 let state = state.mint.check_state(&payload).await.map_err(|err| {
530 tracing::error!("Could not check state of proofs");
531 into_response(err)
532 })?;
533
534 Ok(Json(state))
535}
536
537#[cfg_attr(feature = "swagger", utoipa::path(
538 get,
539 context_path = "/v1",
540 path = "/info",
541 responses(
542 (status = 200, description = "Successful response", body = MintInfo)
543 )
544))]
545#[instrument(skip_all)]
547pub(crate) async fn get_mint_info(
548 State(state): State<MintState>,
549) -> Result<Json<MintInfo>, Response> {
550 Ok(Json(
551 state
552 .mint
553 .mint_info()
554 .await
555 .map_err(|err| {
556 tracing::error!("Could not get mint info: {}", err);
557 into_response(err)
558 })?
559 .clone()
560 .time(unix_time()),
561 ))
562}
563
564#[cfg_attr(feature = "swagger", utoipa::path(
565 post,
566 context_path = "/v1",
567 path = "/swap",
568 request_body(content = SwapRequest, description = "Swap params", content_type = "application/json"),
569 responses(
570 (status = 200, description = "Successful response", body = SwapResponse, content_type = "application/json"),
571 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
572 )
573))]
574#[instrument(skip_all, fields(inputs_count = ?payload.inputs().len()))]
580pub(crate) async fn post_swap(
581 #[cfg(feature = "auth")] auth: AuthHeader,
582 State(state): State<MintState>,
583 Json(payload): Json<SwapRequest>,
584) -> Result<Json<SwapResponse>, Response> {
585 #[cfg(feature = "auth")]
586 {
587 state
588 .mint
589 .verify_auth(
590 auth.into(),
591 &ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
592 )
593 .await
594 .map_err(into_response)?;
595 }
596
597 let swap_response = state
598 .mint
599 .process_swap_request(payload)
600 .await
601 .map_err(|err| {
602 tracing::error!("Could not process swap request: {}", err);
603 into_response(err)
604 })?;
605
606 Ok(Json(swap_response))
607}
608
609#[cfg_attr(feature = "swagger", utoipa::path(
610 post,
611 context_path = "/v1",
612 path = "/restore",
613 request_body(content = RestoreRequest, description = "Restore params", content_type = "application/json"),
614 responses(
615 (status = 200, description = "Successful response", body = RestoreResponse, content_type = "application/json"),
616 (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
617 )
618))]
619#[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
621pub(crate) async fn post_restore(
622 #[cfg(feature = "auth")] auth: AuthHeader,
623 State(state): State<MintState>,
624 Json(payload): Json<RestoreRequest>,
625) -> Result<Json<RestoreResponse>, Response> {
626 #[cfg(feature = "auth")]
627 {
628 state
629 .mint
630 .verify_auth(
631 auth.into(),
632 &ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
633 )
634 .await
635 .map_err(into_response)?;
636 }
637
638 let restore_response = state.mint.restore(payload).await.map_err(|err| {
639 tracing::error!("Could not process restore: {}", err);
640 into_response(err)
641 })?;
642
643 Ok(Json(restore_response))
644}
645
646#[instrument(skip_all)]
647pub(crate) fn into_response<T>(error: T) -> Response
648where
649 T: Into<ErrorResponse>,
650{
651 let err_response: ErrorResponse = error.into();
652 let status_code = match err_response.code {
653 ErrorCode::TokenAlreadySpent
655 | ErrorCode::TokenPending
656 | ErrorCode::QuoteNotPaid
657 | ErrorCode::QuoteExpired
658 | ErrorCode::QuotePending
659 | ErrorCode::KeysetNotFound
660 | ErrorCode::KeysetInactive
661 | ErrorCode::BlindedMessageAlreadySigned
662 | ErrorCode::UnsupportedUnit
663 | ErrorCode::TokensAlreadyIssued
664 | ErrorCode::MintingDisabled
665 | ErrorCode::InvoiceAlreadyPaid
666 | ErrorCode::TokenNotVerified
667 | ErrorCode::TransactionUnbalanced
668 | ErrorCode::AmountOutofLimitRange
669 | ErrorCode::WitnessMissingOrInvalid
670 | ErrorCode::DuplicateSignature
671 | ErrorCode::DuplicateInputs
672 | ErrorCode::DuplicateOutputs
673 | ErrorCode::MultipleUnits
674 | ErrorCode::UnitMismatch
675 | ErrorCode::ClearAuthRequired
676 | ErrorCode::BlindAuthRequired => StatusCode::BAD_REQUEST,
677
678 ErrorCode::ClearAuthFailed | ErrorCode::BlindAuthFailed => StatusCode::UNAUTHORIZED,
680
681 ErrorCode::LightningError | ErrorCode::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
683 };
684
685 (status_code, Json(err_response)).into_response()
686}