router_handlers.rs 20 KB

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