router_handlers.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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, MintInfo,
  11. RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
  12. };
  13. use cdk::util::unix_time;
  14. use paste::paste;
  15. use tracing::instrument;
  16. #[cfg(feature = "auth")]
  17. use crate::auth::AuthHeader;
  18. use crate::ws::main_websocket;
  19. use crate::MintState;
  20. /// Macro to add cache to endpoint
  21. #[macro_export]
  22. macro_rules! post_cache_wrapper {
  23. ($handler:ident, $request_type:ty, $response_type:ty) => {
  24. paste! {
  25. /// Cache wrapper function for $handler:
  26. /// Wrap $handler into a function that caches responses using the request as key
  27. pub async fn [<cache_ $handler>](
  28. #[cfg(feature = "auth")] auth: AuthHeader,
  29. state: State<MintState>,
  30. payload: Json<$request_type>
  31. ) -> Result<Json<$response_type>, Response> {
  32. use std::ops::Deref;
  33. let json_extracted_payload = payload.deref();
  34. let State(mint_state) = state.clone();
  35. let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
  36. Some(key) => key,
  37. None => {
  38. // Could not calculate key, just return the handler result
  39. #[cfg(feature = "auth")]
  40. return $handler(auth, state, payload).await;
  41. #[cfg(not(feature = "auth"))]
  42. return $handler( state, payload).await;
  43. }
  44. };
  45. if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
  46. return Ok(Json(cached_response));
  47. }
  48. #[cfg(feature = "auth")]
  49. let response = $handler(auth, state, payload).await?;
  50. #[cfg(not(feature = "auth"))]
  51. let response = $handler(state, payload).await?;
  52. mint_state.cache.set(cache_key, &response.deref()).await;
  53. Ok(response)
  54. }
  55. }
  56. };
  57. }
  58. /// Macro to add cache to endpoint with prefer header support (for async operations)
  59. #[macro_export]
  60. macro_rules! post_cache_wrapper_with_prefer {
  61. ($handler:ident, $request_type:ty, $response_type:ty) => {
  62. paste! {
  63. /// Cache wrapper function for $handler with PreferHeader support:
  64. /// Wrap $handler into a function that caches responses using the request as key
  65. pub async fn [<cache_ $handler>](
  66. #[cfg(feature = "auth")] auth: AuthHeader,
  67. prefer: PreferHeader,
  68. state: State<MintState>,
  69. payload: Json<$request_type>
  70. ) -> Result<Json<$response_type>, Response> {
  71. use std::ops::Deref;
  72. let json_extracted_payload = payload.deref();
  73. let State(mint_state) = state.clone();
  74. let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
  75. Some(key) => key,
  76. None => {
  77. // Could not calculate key, just return the handler result
  78. #[cfg(feature = "auth")]
  79. return $handler(auth, prefer, state, payload).await;
  80. #[cfg(not(feature = "auth"))]
  81. return $handler(prefer, state, payload).await;
  82. }
  83. };
  84. if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
  85. return Ok(Json(cached_response));
  86. }
  87. #[cfg(feature = "auth")]
  88. let response = $handler(auth, prefer, state, payload).await?;
  89. #[cfg(not(feature = "auth"))]
  90. let response = $handler(prefer, state, payload).await?;
  91. mint_state.cache.set(cache_key, &response.deref()).await;
  92. Ok(response)
  93. }
  94. }
  95. };
  96. }
  97. post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
  98. #[cfg_attr(feature = "swagger", utoipa::path(
  99. get,
  100. context_path = "/v1",
  101. path = "/keys",
  102. responses(
  103. (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json")
  104. )
  105. ))]
  106. /// Get the public keys of the newest mint keyset
  107. ///
  108. /// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
  109. #[instrument(skip_all)]
  110. pub(crate) async fn get_keys(
  111. State(state): State<MintState>,
  112. ) -> Result<Json<KeysResponse>, Response> {
  113. Ok(Json(state.mint.pubkeys()))
  114. }
  115. #[cfg_attr(feature = "swagger", utoipa::path(
  116. get,
  117. context_path = "/v1",
  118. path = "/keys/{keyset_id}",
  119. params(
  120. ("keyset_id" = String, description = "The keyset ID"),
  121. ),
  122. responses(
  123. (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json"),
  124. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  125. )
  126. ))]
  127. /// Get the public keys of a specific keyset
  128. ///
  129. /// Get the public keys of the mint from a specific keyset ID.
  130. #[instrument(skip_all, fields(keyset_id = ?keyset_id))]
  131. pub(crate) async fn get_keyset_pubkeys(
  132. State(state): State<MintState>,
  133. Path(keyset_id): Path<Id>,
  134. ) -> Result<Json<KeysResponse>, Response> {
  135. let pubkeys = state.mint.keyset_pubkeys(&keyset_id).map_err(|err| {
  136. tracing::error!("Could not get keyset pubkeys: {}", err);
  137. into_response(err)
  138. })?;
  139. Ok(Json(pubkeys))
  140. }
  141. #[cfg_attr(feature = "swagger", utoipa::path(
  142. get,
  143. context_path = "/v1",
  144. path = "/keysets",
  145. responses(
  146. (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"),
  147. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  148. )
  149. ))]
  150. /// Get all active keyset IDs of the mint
  151. ///
  152. /// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
  153. #[instrument(skip_all)]
  154. pub(crate) async fn get_keysets(
  155. State(state): State<MintState>,
  156. ) -> Result<Json<KeysetResponse>, Response> {
  157. Ok(Json(state.mint.keysets()))
  158. }
  159. #[instrument(skip_all)]
  160. pub(crate) async fn ws_handler(
  161. #[cfg(feature = "auth")] auth: AuthHeader,
  162. State(state): State<MintState>,
  163. ws: WebSocketUpgrade,
  164. ) -> Result<impl IntoResponse, Response> {
  165. #[cfg(feature = "auth")]
  166. {
  167. state
  168. .mint
  169. .verify_auth(
  170. auth.into(),
  171. &ProtectedEndpoint::new(Method::Get, RoutePath::Ws),
  172. )
  173. .await
  174. .map_err(into_response)?;
  175. }
  176. Ok(ws.on_upgrade(|ws| main_websocket(ws, state)))
  177. }
  178. #[cfg_attr(feature = "swagger", utoipa::path(
  179. post,
  180. context_path = "/v1",
  181. path = "/checkstate",
  182. request_body(content = CheckStateRequest, description = "State params", content_type = "application/json"),
  183. responses(
  184. (status = 200, description = "Successful response", body = CheckStateResponse, content_type = "application/json"),
  185. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  186. )
  187. ))]
  188. /// Check whether a proof is spent already or is pending in a transaction
  189. ///
  190. /// Check whether a secret has been spent already or not.
  191. #[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
  192. pub(crate) async fn post_check(
  193. #[cfg(feature = "auth")] auth: AuthHeader,
  194. State(state): State<MintState>,
  195. Json(payload): Json<CheckStateRequest>,
  196. ) -> Result<Json<CheckStateResponse>, Response> {
  197. #[cfg(feature = "auth")]
  198. {
  199. state
  200. .mint
  201. .verify_auth(
  202. auth.into(),
  203. &ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
  204. )
  205. .await
  206. .map_err(into_response)?;
  207. }
  208. let state = state.mint.check_state(&payload).await.map_err(|err| {
  209. tracing::error!("Could not check state of proofs");
  210. into_response(err)
  211. })?;
  212. Ok(Json(state))
  213. }
  214. #[cfg_attr(feature = "swagger", utoipa::path(
  215. get,
  216. context_path = "/v1",
  217. path = "/info",
  218. responses(
  219. (status = 200, description = "Successful response", body = MintInfo)
  220. )
  221. ))]
  222. /// Mint information, operator contact information, and other info
  223. #[instrument(skip_all)]
  224. pub(crate) async fn get_mint_info(
  225. State(state): State<MintState>,
  226. ) -> Result<Json<MintInfo>, Response> {
  227. Ok(Json(
  228. state
  229. .mint
  230. .mint_info()
  231. .await
  232. .map_err(|err| {
  233. tracing::error!("Could not get mint info: {}", err);
  234. into_response(err)
  235. })?
  236. .clone()
  237. .time(unix_time()),
  238. ))
  239. }
  240. #[cfg_attr(feature = "swagger", utoipa::path(
  241. post,
  242. context_path = "/v1",
  243. path = "/swap",
  244. request_body(content = SwapRequest, description = "Swap params", content_type = "application/json"),
  245. responses(
  246. (status = 200, description = "Successful response", body = SwapResponse, content_type = "application/json"),
  247. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  248. )
  249. ))]
  250. /// Swap inputs for outputs of the same value
  251. ///
  252. /// Requests a set of Proofs to be swapped for another set of BlindSignatures.
  253. ///
  254. /// 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.
  255. #[instrument(skip_all, fields(inputs_count = ?payload.inputs().len()))]
  256. pub(crate) async fn post_swap(
  257. #[cfg(feature = "auth")] auth: AuthHeader,
  258. State(state): State<MintState>,
  259. Json(payload): Json<SwapRequest>,
  260. ) -> Result<Json<SwapResponse>, Response> {
  261. #[cfg(feature = "auth")]
  262. {
  263. state
  264. .mint
  265. .verify_auth(
  266. auth.into(),
  267. &ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
  268. )
  269. .await
  270. .map_err(into_response)?;
  271. }
  272. let swap_response = state
  273. .mint
  274. .process_swap_request(payload)
  275. .await
  276. .map_err(|err| {
  277. tracing::error!("Could not process swap request: {}", err);
  278. into_response(err)
  279. })?;
  280. Ok(Json(swap_response))
  281. }
  282. #[cfg_attr(feature = "swagger", utoipa::path(
  283. post,
  284. context_path = "/v1",
  285. path = "/restore",
  286. request_body(content = RestoreRequest, description = "Restore params", content_type = "application/json"),
  287. responses(
  288. (status = 200, description = "Successful response", body = RestoreResponse, content_type = "application/json"),
  289. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  290. )
  291. ))]
  292. /// Restores blind signature for a set of outputs.
  293. #[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
  294. pub(crate) async fn post_restore(
  295. #[cfg(feature = "auth")] auth: AuthHeader,
  296. State(state): State<MintState>,
  297. Json(payload): Json<RestoreRequest>,
  298. ) -> Result<Json<RestoreResponse>, Response> {
  299. #[cfg(feature = "auth")]
  300. {
  301. state
  302. .mint
  303. .verify_auth(
  304. auth.into(),
  305. &ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
  306. )
  307. .await
  308. .map_err(into_response)?;
  309. }
  310. let restore_response = state.mint.restore(payload).await.map_err(|err| {
  311. tracing::error!("Could not process restore: {}", err);
  312. into_response(err)
  313. })?;
  314. Ok(Json(restore_response))
  315. }
  316. #[instrument(skip_all)]
  317. pub(crate) fn into_response<T>(error: T) -> Response
  318. where
  319. T: Into<ErrorResponse>,
  320. {
  321. let err_response: ErrorResponse = error.into();
  322. let status_code = match err_response.code {
  323. // Client errors (400 Bad Request)
  324. ErrorCode::TokenAlreadySpent
  325. | ErrorCode::TokenPending
  326. | ErrorCode::QuoteNotPaid
  327. | ErrorCode::QuoteExpired
  328. | ErrorCode::QuotePending
  329. | ErrorCode::KeysetNotFound
  330. | ErrorCode::KeysetInactive
  331. | ErrorCode::BlindedMessageAlreadySigned
  332. | ErrorCode::UnsupportedUnit
  333. | ErrorCode::TokensAlreadyIssued
  334. | ErrorCode::MintingDisabled
  335. | ErrorCode::InvoiceAlreadyPaid
  336. | ErrorCode::TokenNotVerified
  337. | ErrorCode::TransactionUnbalanced
  338. | ErrorCode::AmountOutofLimitRange
  339. | ErrorCode::WitnessMissingOrInvalid
  340. | ErrorCode::DuplicateSignature
  341. | ErrorCode::DuplicateInputs
  342. | ErrorCode::DuplicateOutputs
  343. | ErrorCode::MultipleUnits
  344. | ErrorCode::UnitMismatch
  345. | ErrorCode::ClearAuthRequired
  346. | ErrorCode::BlindAuthRequired => StatusCode::BAD_REQUEST,
  347. // Auth failures (401 Unauthorized)
  348. ErrorCode::ClearAuthFailed | ErrorCode::BlindAuthFailed => StatusCode::UNAUTHORIZED,
  349. // Lightning/payment errors and unknown errors (500 Internal Server Error)
  350. ErrorCode::LightningError | ErrorCode::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
  351. };
  352. (status_code, Json(err_response)).into_response()
  353. }