router_handlers.rs 18 KB

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