router_handlers.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  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. #[cfg(feature = "auth")]
  8. use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
  9. use cdk::nuts::{
  10. CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
  11. MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
  12. MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
  13. SwapRequest, SwapResponse,
  14. };
  15. use cdk::util::unix_time;
  16. use paste::paste;
  17. use tracing::instrument;
  18. use uuid::Uuid;
  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<Uuid>, MintResponse);
  63. post_cache_wrapper!(
  64. post_melt_bolt11,
  65. MeltRequest<Uuid>,
  66. MeltQuoteBolt11Response<Uuid>
  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<Uuid>>, 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<Uuid>,
  184. ) -> Result<Json<MintQuoteBolt11Response<Uuid>>, 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. State(state): State<MintState>,
  209. ws: WebSocketUpgrade,
  210. ) -> impl IntoResponse {
  211. ws.on_upgrade(|ws| main_websocket(ws, state))
  212. }
  213. /// Mint tokens by paying a BOLT11 Lightning invoice.
  214. ///
  215. /// Requests the minting of tokens belonging to a paid payment request.
  216. ///
  217. /// Call this endpoint after `POST /v1/mint/quote`.
  218. #[cfg_attr(feature = "swagger", utoipa::path(
  219. post,
  220. context_path = "/v1",
  221. path = "/mint/bolt11",
  222. request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
  223. responses(
  224. (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
  225. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  226. )
  227. ))]
  228. #[instrument(skip_all, fields(quote_id = ?payload.quote))]
  229. pub(crate) async fn post_mint_bolt11(
  230. #[cfg(feature = "auth")] auth: AuthHeader,
  231. State(state): State<MintState>,
  232. Json(payload): Json<MintRequest<Uuid>>,
  233. ) -> Result<Json<MintResponse>, Response> {
  234. #[cfg(feature = "auth")]
  235. {
  236. state
  237. .mint
  238. .verify_auth(
  239. auth.into(),
  240. &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
  241. )
  242. .await
  243. .map_err(into_response)?;
  244. }
  245. let res = state
  246. .mint
  247. .process_mint_request(payload)
  248. .await
  249. .map_err(|err| {
  250. tracing::error!("Could not process mint: {}", err);
  251. into_response(err)
  252. })?;
  253. Ok(Json(res))
  254. }
  255. #[cfg_attr(feature = "swagger", utoipa::path(
  256. post,
  257. context_path = "/v1",
  258. path = "/melt/quote/bolt11",
  259. request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"),
  260. responses(
  261. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  262. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  263. )
  264. ))]
  265. #[instrument(skip_all)]
  266. /// Request a quote for melting tokens
  267. pub(crate) async fn post_melt_bolt11_quote(
  268. #[cfg(feature = "auth")] auth: AuthHeader,
  269. State(state): State<MintState>,
  270. Json(payload): Json<MeltQuoteBolt11Request>,
  271. ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
  272. #[cfg(feature = "auth")]
  273. {
  274. state
  275. .mint
  276. .verify_auth(
  277. auth.into(),
  278. &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
  279. )
  280. .await
  281. .map_err(into_response)?;
  282. }
  283. let quote = state
  284. .mint
  285. .get_melt_quote(payload.into())
  286. .await
  287. .map_err(into_response)?;
  288. Ok(Json(quote))
  289. }
  290. #[cfg_attr(feature = "swagger", utoipa::path(
  291. get,
  292. context_path = "/v1",
  293. path = "/melt/quote/bolt11/{quote_id}",
  294. params(
  295. ("quote_id" = String, description = "The quote ID"),
  296. ),
  297. responses(
  298. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  299. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  300. )
  301. ))]
  302. /// Get melt quote by ID
  303. ///
  304. /// Get melt quote state.
  305. #[instrument(skip_all, fields(quote_id = ?quote_id))]
  306. pub(crate) async fn get_check_melt_bolt11_quote(
  307. #[cfg(feature = "auth")] auth: AuthHeader,
  308. State(state): State<MintState>,
  309. Path(quote_id): Path<Uuid>,
  310. ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
  311. #[cfg(feature = "auth")]
  312. {
  313. state
  314. .mint
  315. .verify_auth(
  316. auth.into(),
  317. &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
  318. )
  319. .await
  320. .map_err(into_response)?;
  321. }
  322. let quote = state
  323. .mint
  324. .check_melt_quote(&quote_id)
  325. .await
  326. .map_err(|err| {
  327. tracing::error!("Could not check melt quote: {}", err);
  328. into_response(err)
  329. })?;
  330. Ok(Json(quote))
  331. }
  332. #[cfg_attr(feature = "swagger", utoipa::path(
  333. post,
  334. context_path = "/v1",
  335. path = "/melt/bolt11",
  336. request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
  337. responses(
  338. (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
  339. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  340. )
  341. ))]
  342. /// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
  343. ///
  344. /// Requests tokens to be destroyed and sent out via Lightning.
  345. #[instrument(skip_all)]
  346. pub(crate) async fn post_melt_bolt11(
  347. #[cfg(feature = "auth")] auth: AuthHeader,
  348. State(state): State<MintState>,
  349. Json(payload): Json<MeltRequest<Uuid>>,
  350. ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
  351. #[cfg(feature = "auth")]
  352. {
  353. state
  354. .mint
  355. .verify_auth(
  356. auth.into(),
  357. &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
  358. )
  359. .await
  360. .map_err(into_response)?;
  361. }
  362. let res = state.mint.melt(&payload).await.map_err(into_response)?;
  363. Ok(Json(res))
  364. }
  365. #[cfg_attr(feature = "swagger", utoipa::path(
  366. post,
  367. context_path = "/v1",
  368. path = "/checkstate",
  369. request_body(content = CheckStateRequest, description = "State params", content_type = "application/json"),
  370. responses(
  371. (status = 200, description = "Successful response", body = CheckStateResponse, content_type = "application/json"),
  372. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  373. )
  374. ))]
  375. /// Check whether a proof is spent already or is pending in a transaction
  376. ///
  377. /// Check whether a secret has been spent already or not.
  378. #[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
  379. pub(crate) async fn post_check(
  380. #[cfg(feature = "auth")] auth: AuthHeader,
  381. State(state): State<MintState>,
  382. Json(payload): Json<CheckStateRequest>,
  383. ) -> Result<Json<CheckStateResponse>, Response> {
  384. #[cfg(feature = "auth")]
  385. {
  386. state
  387. .mint
  388. .verify_auth(
  389. auth.into(),
  390. &ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
  391. )
  392. .await
  393. .map_err(into_response)?;
  394. }
  395. let state = state.mint.check_state(&payload).await.map_err(|err| {
  396. tracing::error!("Could not check state of proofs");
  397. into_response(err)
  398. })?;
  399. Ok(Json(state))
  400. }
  401. #[cfg_attr(feature = "swagger", utoipa::path(
  402. get,
  403. context_path = "/v1",
  404. path = "/info",
  405. responses(
  406. (status = 200, description = "Successful response", body = MintInfo)
  407. )
  408. ))]
  409. /// Mint information, operator contact information, and other info
  410. #[instrument(skip_all)]
  411. pub(crate) async fn get_mint_info(
  412. State(state): State<MintState>,
  413. ) -> Result<Json<MintInfo>, Response> {
  414. Ok(Json(
  415. state
  416. .mint
  417. .mint_info()
  418. .await
  419. .map_err(|err| {
  420. tracing::error!("Could not get mint info: {}", err);
  421. into_response(err)
  422. })?
  423. .clone()
  424. .time(unix_time()),
  425. ))
  426. }
  427. #[cfg_attr(feature = "swagger", utoipa::path(
  428. post,
  429. context_path = "/v1",
  430. path = "/swap",
  431. request_body(content = SwapRequest, description = "Swap params", content_type = "application/json"),
  432. responses(
  433. (status = 200, description = "Successful response", body = SwapResponse, content_type = "application/json"),
  434. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  435. )
  436. ))]
  437. /// Swap inputs for outputs of the same value
  438. ///
  439. /// Requests a set of Proofs to be swapped for another set of BlindSignatures.
  440. ///
  441. /// 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.
  442. #[instrument(skip_all, fields(inputs_count = ?payload.inputs().len()))]
  443. pub(crate) async fn post_swap(
  444. #[cfg(feature = "auth")] auth: AuthHeader,
  445. State(state): State<MintState>,
  446. Json(payload): Json<SwapRequest>,
  447. ) -> Result<Json<SwapResponse>, Response> {
  448. #[cfg(feature = "auth")]
  449. {
  450. state
  451. .mint
  452. .verify_auth(
  453. auth.into(),
  454. &ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
  455. )
  456. .await
  457. .map_err(into_response)?;
  458. }
  459. let swap_response = state
  460. .mint
  461. .process_swap_request(payload)
  462. .await
  463. .map_err(|err| {
  464. tracing::error!("Could not process swap request: {}", err);
  465. into_response(err)
  466. })?;
  467. Ok(Json(swap_response))
  468. }
  469. #[cfg_attr(feature = "swagger", utoipa::path(
  470. post,
  471. context_path = "/v1",
  472. path = "/restore",
  473. request_body(content = RestoreRequest, description = "Restore params", content_type = "application/json"),
  474. responses(
  475. (status = 200, description = "Successful response", body = RestoreResponse, content_type = "application/json"),
  476. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  477. )
  478. ))]
  479. /// Restores blind signature for a set of outputs.
  480. #[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
  481. pub(crate) async fn post_restore(
  482. #[cfg(feature = "auth")] auth: AuthHeader,
  483. State(state): State<MintState>,
  484. Json(payload): Json<RestoreRequest>,
  485. ) -> Result<Json<RestoreResponse>, Response> {
  486. #[cfg(feature = "auth")]
  487. {
  488. state
  489. .mint
  490. .verify_auth(
  491. auth.into(),
  492. &ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
  493. )
  494. .await
  495. .map_err(into_response)?;
  496. }
  497. let restore_response = state.mint.restore(payload).await.map_err(|err| {
  498. tracing::error!("Could not process restore: {}", err);
  499. into_response(err)
  500. })?;
  501. Ok(Json(restore_response))
  502. }
  503. #[instrument(skip_all)]
  504. pub(crate) fn into_response<T>(error: T) -> Response
  505. where
  506. T: Into<ErrorResponse>,
  507. {
  508. let err_response: ErrorResponse = error.into();
  509. let status_code = match err_response.code {
  510. // Client errors (400 Bad Request)
  511. ErrorCode::TokenAlreadySpent
  512. | ErrorCode::TokenPending
  513. | ErrorCode::QuoteNotPaid
  514. | ErrorCode::QuoteExpired
  515. | ErrorCode::QuotePending
  516. | ErrorCode::KeysetNotFound
  517. | ErrorCode::KeysetInactive
  518. | ErrorCode::BlindedMessageAlreadySigned
  519. | ErrorCode::UnsupportedUnit
  520. | ErrorCode::TokensAlreadyIssued
  521. | ErrorCode::MintingDisabled
  522. | ErrorCode::InvoiceAlreadyPaid
  523. | ErrorCode::TokenNotVerified
  524. | ErrorCode::TransactionUnbalanced
  525. | ErrorCode::AmountOutofLimitRange
  526. | ErrorCode::WitnessMissingOrInvalid
  527. | ErrorCode::DuplicateInputs
  528. | ErrorCode::DuplicateOutputs
  529. | ErrorCode::MultipleUnits
  530. | ErrorCode::UnitMismatch
  531. | ErrorCode::ClearAuthRequired
  532. | ErrorCode::BlindAuthRequired => StatusCode::BAD_REQUEST,
  533. // Auth failures (401 Unauthorized)
  534. ErrorCode::ClearAuthFailed | ErrorCode::BlindAuthFailed => StatusCode::UNAUTHORIZED,
  535. // Lightning/payment errors and unknown errors (500 Internal Server Error)
  536. ErrorCode::LightningError | ErrorCode::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
  537. };
  538. (status_code, Json(err_response)).into_response()
  539. }