router_handlers.rs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. use anyhow::Result;
  2. use axum::extract::ws::WebSocketUpgrade;
  3. use axum::extract::{FromRequestParts, Json, Path, State};
  4. use axum::http::request::Parts;
  5. use axum::http::StatusCode;
  6. use axum::response::{IntoResponse, Response};
  7. use cdk::error::{ErrorCode, ErrorResponse};
  8. use cdk::mint::QuoteId;
  9. #[cfg(feature = "auth")]
  10. use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
  11. use cdk::nuts::{
  12. CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
  13. MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
  14. MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
  15. SwapRequest, SwapResponse,
  16. };
  17. use cdk::util::unix_time;
  18. use paste::paste;
  19. use tracing::instrument;
  20. #[cfg(feature = "auth")]
  21. use crate::auth::AuthHeader;
  22. use crate::ws::main_websocket;
  23. use crate::MintState;
  24. const PREFER_HEADER_KEY: &str = "Prefer";
  25. /// Header extractor for the Prefer header
  26. ///
  27. /// This extractor checks for the `Prefer: respond-async` header
  28. /// to determine if the client wants asynchronous processing
  29. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  30. pub struct PreferHeader {
  31. pub respond_async: bool,
  32. }
  33. impl<S> FromRequestParts<S> for PreferHeader
  34. where
  35. S: Send + Sync,
  36. {
  37. type Rejection = (StatusCode, String);
  38. async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
  39. // Check for Prefer header
  40. if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
  41. let value = prefer_value.to_str().map_err(|_| {
  42. (
  43. StatusCode::BAD_REQUEST,
  44. "Invalid Prefer header value".to_string(),
  45. )
  46. })?;
  47. // Check if it contains "respond-async"
  48. let respond_async = value.to_lowercase().contains("respond-async");
  49. return Ok(PreferHeader { respond_async });
  50. }
  51. // No Prefer header found - default to synchronous processing
  52. Ok(PreferHeader {
  53. respond_async: false,
  54. })
  55. }
  56. }
  57. /// Macro to add cache to endpoint
  58. #[macro_export]
  59. macro_rules! post_cache_wrapper {
  60. ($handler:ident, $request_type:ty, $response_type:ty) => {
  61. paste! {
  62. /// Cache wrapper function for $handler:
  63. /// Wrap $handler into a function that caches responses using the request as key
  64. pub async fn [<cache_ $handler>](
  65. #[cfg(feature = "auth")] auth: AuthHeader,
  66. state: State<MintState>,
  67. payload: Json<$request_type>
  68. ) -> Result<Json<$response_type>, Response> {
  69. use std::ops::Deref;
  70. let json_extracted_payload = payload.deref();
  71. let State(mint_state) = state.clone();
  72. let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
  73. Some(key) => key,
  74. None => {
  75. // Could not calculate key, just return the handler result
  76. #[cfg(feature = "auth")]
  77. return $handler(auth, state, payload).await;
  78. #[cfg(not(feature = "auth"))]
  79. return $handler( state, payload).await;
  80. }
  81. };
  82. if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
  83. return Ok(Json(cached_response));
  84. }
  85. #[cfg(feature = "auth")]
  86. let response = $handler(auth, state, payload).await?;
  87. #[cfg(not(feature = "auth"))]
  88. let response = $handler(state, payload).await?;
  89. mint_state.cache.set(cache_key, &response.deref()).await;
  90. Ok(response)
  91. }
  92. }
  93. };
  94. }
  95. /// Macro to add cache to endpoint with prefer header support (for async operations)
  96. #[macro_export]
  97. macro_rules! post_cache_wrapper_with_prefer {
  98. ($handler:ident, $request_type:ty, $response_type:ty) => {
  99. paste! {
  100. /// Cache wrapper function for $handler with PreferHeader support:
  101. /// Wrap $handler into a function that caches responses using the request as key
  102. pub async fn [<cache_ $handler>](
  103. #[cfg(feature = "auth")] auth: AuthHeader,
  104. prefer: PreferHeader,
  105. state: State<MintState>,
  106. payload: Json<$request_type>
  107. ) -> Result<Json<$response_type>, Response> {
  108. use std::ops::Deref;
  109. let json_extracted_payload = payload.deref();
  110. let State(mint_state) = state.clone();
  111. let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
  112. Some(key) => key,
  113. None => {
  114. // Could not calculate key, just return the handler result
  115. #[cfg(feature = "auth")]
  116. return $handler(auth, prefer, state, payload).await;
  117. #[cfg(not(feature = "auth"))]
  118. return $handler(prefer, state, payload).await;
  119. }
  120. };
  121. if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
  122. return Ok(Json(cached_response));
  123. }
  124. #[cfg(feature = "auth")]
  125. let response = $handler(auth, prefer, state, payload).await?;
  126. #[cfg(not(feature = "auth"))]
  127. let response = $handler(prefer, state, payload).await?;
  128. mint_state.cache.set(cache_key, &response.deref()).await;
  129. Ok(response)
  130. }
  131. }
  132. };
  133. }
  134. post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
  135. post_cache_wrapper!(post_mint_bolt11, MintRequest<QuoteId>, MintResponse);
  136. post_cache_wrapper_with_prefer!(
  137. post_melt_bolt11,
  138. MeltRequest<QuoteId>,
  139. MeltQuoteBolt11Response<QuoteId>
  140. );
  141. #[cfg_attr(feature = "swagger", utoipa::path(
  142. get,
  143. context_path = "/v1",
  144. path = "/keys",
  145. responses(
  146. (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json")
  147. )
  148. ))]
  149. /// Get the public keys of the newest mint keyset
  150. ///
  151. /// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
  152. #[instrument(skip_all)]
  153. pub(crate) async fn get_keys(
  154. State(state): State<MintState>,
  155. ) -> Result<Json<KeysResponse>, Response> {
  156. Ok(Json(state.mint.pubkeys()))
  157. }
  158. #[cfg_attr(feature = "swagger", utoipa::path(
  159. get,
  160. context_path = "/v1",
  161. path = "/keys/{keyset_id}",
  162. params(
  163. ("keyset_id" = String, description = "The keyset ID"),
  164. ),
  165. responses(
  166. (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json"),
  167. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  168. )
  169. ))]
  170. /// Get the public keys of a specific keyset
  171. ///
  172. /// Get the public keys of the mint from a specific keyset ID.
  173. #[instrument(skip_all, fields(keyset_id = ?keyset_id))]
  174. pub(crate) async fn get_keyset_pubkeys(
  175. State(state): State<MintState>,
  176. Path(keyset_id): Path<Id>,
  177. ) -> Result<Json<KeysResponse>, Response> {
  178. let pubkeys = state.mint.keyset_pubkeys(&keyset_id).map_err(|err| {
  179. tracing::error!("Could not get keyset pubkeys: {}", err);
  180. into_response(err)
  181. })?;
  182. Ok(Json(pubkeys))
  183. }
  184. #[cfg_attr(feature = "swagger", utoipa::path(
  185. get,
  186. context_path = "/v1",
  187. path = "/keysets",
  188. responses(
  189. (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"),
  190. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  191. )
  192. ))]
  193. /// Get all active keyset IDs of the mint
  194. ///
  195. /// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
  196. #[instrument(skip_all)]
  197. pub(crate) async fn get_keysets(
  198. State(state): State<MintState>,
  199. ) -> Result<Json<KeysetResponse>, Response> {
  200. Ok(Json(state.mint.keysets()))
  201. }
  202. #[cfg_attr(feature = "swagger", utoipa::path(
  203. post,
  204. context_path = "/v1",
  205. path = "/mint/quote/bolt11",
  206. request_body(content = MintQuoteBolt11Request, description = "Request params", content_type = "application/json"),
  207. responses(
  208. (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
  209. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  210. )
  211. ))]
  212. /// Request a quote for minting of new tokens
  213. ///
  214. /// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow.
  215. #[instrument(skip_all, fields(amount = ?payload.amount))]
  216. pub(crate) async fn post_mint_bolt11_quote(
  217. #[cfg(feature = "auth")] auth: AuthHeader,
  218. State(state): State<MintState>,
  219. Json(payload): Json<MintQuoteBolt11Request>,
  220. ) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
  221. #[cfg(feature = "auth")]
  222. state
  223. .mint
  224. .verify_auth(
  225. auth.into(),
  226. &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
  227. )
  228. .await
  229. .map_err(into_response)?;
  230. let quote = state
  231. .mint
  232. .get_mint_quote(payload.into())
  233. .await
  234. .map_err(into_response)?;
  235. Ok(Json(quote.try_into().map_err(into_response)?))
  236. }
  237. #[cfg_attr(feature = "swagger", utoipa::path(
  238. get,
  239. context_path = "/v1",
  240. path = "/mint/quote/bolt11/{quote_id}",
  241. params(
  242. ("quote_id" = String, description = "The quote ID"),
  243. ),
  244. responses(
  245. (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
  246. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  247. )
  248. ))]
  249. /// Get mint quote by ID
  250. ///
  251. /// Get mint quote state.
  252. #[instrument(skip_all, fields(quote_id = ?quote_id))]
  253. pub(crate) async fn get_check_mint_bolt11_quote(
  254. #[cfg(feature = "auth")] auth: AuthHeader,
  255. State(state): State<MintState>,
  256. Path(quote_id): Path<QuoteId>,
  257. ) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
  258. #[cfg(feature = "auth")]
  259. {
  260. state
  261. .mint
  262. .verify_auth(
  263. auth.into(),
  264. &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
  265. )
  266. .await
  267. .map_err(into_response)?;
  268. }
  269. let quote = state
  270. .mint
  271. .check_mint_quote(&quote_id)
  272. .await
  273. .map_err(|err| {
  274. tracing::error!("Could not check mint quote {}: {}", quote_id, err);
  275. into_response(err)
  276. })?;
  277. Ok(Json(quote.try_into().map_err(into_response)?))
  278. }
  279. #[instrument(skip_all)]
  280. pub(crate) async fn ws_handler(
  281. #[cfg(feature = "auth")] auth: AuthHeader,
  282. State(state): State<MintState>,
  283. ws: WebSocketUpgrade,
  284. ) -> Result<impl IntoResponse, Response> {
  285. #[cfg(feature = "auth")]
  286. {
  287. state
  288. .mint
  289. .verify_auth(
  290. auth.into(),
  291. &ProtectedEndpoint::new(Method::Get, RoutePath::Ws),
  292. )
  293. .await
  294. .map_err(into_response)?;
  295. }
  296. Ok(ws.on_upgrade(|ws| main_websocket(ws, state)))
  297. }
  298. /// Mint tokens by paying a BOLT11 Lightning invoice.
  299. ///
  300. /// Requests the minting of tokens belonging to a paid payment request.
  301. ///
  302. /// Call this endpoint after `POST /v1/mint/quote`.
  303. #[cfg_attr(feature = "swagger", utoipa::path(
  304. post,
  305. context_path = "/v1",
  306. path = "/mint/bolt11",
  307. request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
  308. responses(
  309. (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
  310. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  311. )
  312. ))]
  313. #[instrument(skip_all, fields(quote_id = ?payload.quote))]
  314. pub(crate) async fn post_mint_bolt11(
  315. #[cfg(feature = "auth")] auth: AuthHeader,
  316. State(state): State<MintState>,
  317. Json(payload): Json<MintRequest<QuoteId>>,
  318. ) -> Result<Json<MintResponse>, Response> {
  319. #[cfg(feature = "auth")]
  320. {
  321. state
  322. .mint
  323. .verify_auth(
  324. auth.into(),
  325. &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
  326. )
  327. .await
  328. .map_err(into_response)?;
  329. }
  330. let res = state
  331. .mint
  332. .process_mint_request(payload)
  333. .await
  334. .map_err(|err| {
  335. tracing::error!("Could not process mint: {}", err);
  336. into_response(err)
  337. })?;
  338. Ok(Json(res))
  339. }
  340. #[cfg_attr(feature = "swagger", utoipa::path(
  341. post,
  342. context_path = "/v1",
  343. path = "/melt/quote/bolt11",
  344. request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"),
  345. responses(
  346. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  347. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  348. )
  349. ))]
  350. #[instrument(skip_all)]
  351. /// Request a quote for melting tokens
  352. pub(crate) async fn post_melt_bolt11_quote(
  353. #[cfg(feature = "auth")] auth: AuthHeader,
  354. State(state): State<MintState>,
  355. Json(payload): Json<MeltQuoteBolt11Request>,
  356. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  357. #[cfg(feature = "auth")]
  358. {
  359. state
  360. .mint
  361. .verify_auth(
  362. auth.into(),
  363. &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
  364. )
  365. .await
  366. .map_err(into_response)?;
  367. }
  368. let quote = state
  369. .mint
  370. .get_melt_quote(payload.into())
  371. .await
  372. .map_err(into_response)?;
  373. Ok(Json(quote))
  374. }
  375. #[cfg_attr(feature = "swagger", utoipa::path(
  376. get,
  377. context_path = "/v1",
  378. path = "/melt/quote/bolt11/{quote_id}",
  379. params(
  380. ("quote_id" = String, description = "The quote ID"),
  381. ),
  382. responses(
  383. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  384. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  385. )
  386. ))]
  387. /// Get melt quote by ID
  388. ///
  389. /// Get melt quote state.
  390. #[instrument(skip_all, fields(quote_id = ?quote_id))]
  391. pub(crate) async fn get_check_melt_bolt11_quote(
  392. #[cfg(feature = "auth")] auth: AuthHeader,
  393. State(state): State<MintState>,
  394. Path(quote_id): Path<QuoteId>,
  395. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  396. #[cfg(feature = "auth")]
  397. {
  398. state
  399. .mint
  400. .verify_auth(
  401. auth.into(),
  402. &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
  403. )
  404. .await
  405. .map_err(into_response)?;
  406. }
  407. let quote = state
  408. .mint
  409. .check_melt_quote(&quote_id)
  410. .await
  411. .map_err(|err| {
  412. tracing::error!("Could not check melt quote: {}", err);
  413. into_response(err)
  414. })?;
  415. Ok(Json(quote))
  416. }
  417. #[cfg_attr(feature = "swagger", utoipa::path(
  418. post,
  419. context_path = "/v1",
  420. path = "/melt/bolt11",
  421. request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
  422. responses(
  423. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  424. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  425. )
  426. ))]
  427. /// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
  428. ///
  429. /// Requests tokens to be destroyed and sent out via Lightning.
  430. #[instrument(skip_all)]
  431. pub(crate) async fn post_melt_bolt11(
  432. #[cfg(feature = "auth")] auth: AuthHeader,
  433. prefer: PreferHeader,
  434. State(state): State<MintState>,
  435. Json(payload): Json<MeltRequest<QuoteId>>,
  436. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  437. #[cfg(feature = "auth")]
  438. {
  439. state
  440. .mint
  441. .verify_auth(
  442. auth.into(),
  443. &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
  444. )
  445. .await
  446. .map_err(into_response)?;
  447. }
  448. let res = if prefer.respond_async {
  449. // Asynchronous processing - return immediately after setup
  450. state
  451. .mint
  452. .melt_async(&payload)
  453. .await
  454. .map_err(into_response)?
  455. } else {
  456. // Synchronous processing - wait for completion
  457. state.mint.melt(&payload).await.map_err(into_response)?
  458. };
  459. Ok(Json(res))
  460. }
  461. #[cfg_attr(feature = "swagger", utoipa::path(
  462. post,
  463. context_path = "/v1",
  464. path = "/checkstate",
  465. request_body(content = CheckStateRequest, description = "State params", content_type = "application/json"),
  466. responses(
  467. (status = 200, description = "Successful response", body = CheckStateResponse, content_type = "application/json"),
  468. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  469. )
  470. ))]
  471. /// Check whether a proof is spent already or is pending in a transaction
  472. ///
  473. /// Check whether a secret has been spent already or not.
  474. #[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
  475. pub(crate) async fn post_check(
  476. #[cfg(feature = "auth")] auth: AuthHeader,
  477. State(state): State<MintState>,
  478. Json(payload): Json<CheckStateRequest>,
  479. ) -> Result<Json<CheckStateResponse>, Response> {
  480. #[cfg(feature = "auth")]
  481. {
  482. state
  483. .mint
  484. .verify_auth(
  485. auth.into(),
  486. &ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
  487. )
  488. .await
  489. .map_err(into_response)?;
  490. }
  491. let state = state.mint.check_state(&payload).await.map_err(|err| {
  492. tracing::error!("Could not check state of proofs");
  493. into_response(err)
  494. })?;
  495. Ok(Json(state))
  496. }
  497. #[cfg_attr(feature = "swagger", utoipa::path(
  498. get,
  499. context_path = "/v1",
  500. path = "/info",
  501. responses(
  502. (status = 200, description = "Successful response", body = MintInfo)
  503. )
  504. ))]
  505. /// Mint information, operator contact information, and other info
  506. #[instrument(skip_all)]
  507. pub(crate) async fn get_mint_info(
  508. State(state): State<MintState>,
  509. ) -> Result<Json<MintInfo>, Response> {
  510. Ok(Json(
  511. state
  512. .mint
  513. .mint_info()
  514. .await
  515. .map_err(|err| {
  516. tracing::error!("Could not get mint info: {}", err);
  517. into_response(err)
  518. })?
  519. .clone()
  520. .time(unix_time()),
  521. ))
  522. }
  523. #[cfg_attr(feature = "swagger", utoipa::path(
  524. post,
  525. context_path = "/v1",
  526. path = "/swap",
  527. request_body(content = SwapRequest, description = "Swap params", content_type = "application/json"),
  528. responses(
  529. (status = 200, description = "Successful response", body = SwapResponse, content_type = "application/json"),
  530. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  531. )
  532. ))]
  533. /// Swap inputs for outputs of the same value
  534. ///
  535. /// Requests a set of Proofs to be swapped for another set of BlindSignatures.
  536. ///
  537. /// This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. It can then used by Carol to redeem the tokens for new proofs.
  538. #[instrument(skip_all, fields(inputs_count = ?payload.inputs().len()))]
  539. pub(crate) async fn post_swap(
  540. #[cfg(feature = "auth")] auth: AuthHeader,
  541. State(state): State<MintState>,
  542. Json(payload): Json<SwapRequest>,
  543. ) -> Result<Json<SwapResponse>, Response> {
  544. #[cfg(feature = "auth")]
  545. {
  546. state
  547. .mint
  548. .verify_auth(
  549. auth.into(),
  550. &ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
  551. )
  552. .await
  553. .map_err(into_response)?;
  554. }
  555. let swap_response = state
  556. .mint
  557. .process_swap_request(payload)
  558. .await
  559. .map_err(|err| {
  560. tracing::error!("Could not process swap request: {}", err);
  561. into_response(err)
  562. })?;
  563. Ok(Json(swap_response))
  564. }
  565. #[cfg_attr(feature = "swagger", utoipa::path(
  566. post,
  567. context_path = "/v1",
  568. path = "/restore",
  569. request_body(content = RestoreRequest, description = "Restore params", content_type = "application/json"),
  570. responses(
  571. (status = 200, description = "Successful response", body = RestoreResponse, content_type = "application/json"),
  572. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  573. )
  574. ))]
  575. /// Restores blind signature for a set of outputs.
  576. #[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
  577. pub(crate) async fn post_restore(
  578. #[cfg(feature = "auth")] auth: AuthHeader,
  579. State(state): State<MintState>,
  580. Json(payload): Json<RestoreRequest>,
  581. ) -> Result<Json<RestoreResponse>, Response> {
  582. #[cfg(feature = "auth")]
  583. {
  584. state
  585. .mint
  586. .verify_auth(
  587. auth.into(),
  588. &ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
  589. )
  590. .await
  591. .map_err(into_response)?;
  592. }
  593. let restore_response = state.mint.restore(payload).await.map_err(|err| {
  594. tracing::error!("Could not process restore: {}", err);
  595. into_response(err)
  596. })?;
  597. Ok(Json(restore_response))
  598. }
  599. #[instrument(skip_all)]
  600. pub(crate) fn into_response<T>(error: T) -> Response
  601. where
  602. T: Into<ErrorResponse>,
  603. {
  604. let err_response: ErrorResponse = error.into();
  605. let status_code = match err_response.code {
  606. // Client errors (400 Bad Request)
  607. ErrorCode::TokenAlreadySpent
  608. | ErrorCode::TokenPending
  609. | ErrorCode::QuoteNotPaid
  610. | ErrorCode::QuoteExpired
  611. | ErrorCode::QuotePending
  612. | ErrorCode::KeysetNotFound
  613. | ErrorCode::KeysetInactive
  614. | ErrorCode::BlindedMessageAlreadySigned
  615. | ErrorCode::UnsupportedUnit
  616. | ErrorCode::TokensAlreadyIssued
  617. | ErrorCode::MintingDisabled
  618. | ErrorCode::InvoiceAlreadyPaid
  619. | ErrorCode::TokenNotVerified
  620. | ErrorCode::TransactionUnbalanced
  621. | ErrorCode::AmountOutofLimitRange
  622. | ErrorCode::WitnessMissingOrInvalid
  623. | ErrorCode::DuplicateSignature
  624. | ErrorCode::DuplicateInputs
  625. | ErrorCode::DuplicateOutputs
  626. | ErrorCode::MultipleUnits
  627. | ErrorCode::UnitMismatch
  628. | ErrorCode::ClearAuthRequired
  629. | ErrorCode::BlindAuthRequired => StatusCode::BAD_REQUEST,
  630. // Auth failures (401 Unauthorized)
  631. ErrorCode::ClearAuthFailed | ErrorCode::BlindAuthFailed => StatusCode::UNAUTHORIZED,
  632. // Lightning/payment errors and unknown errors (500 Internal Server Error)
  633. ErrorCode::LightningError | ErrorCode::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
  634. };
  635. (status_code, Json(err_response)).into_response()
  636. }