custom_handlers.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. //! Generic handlers for custom payment methods
  2. //!
  3. //! These handlers work for ANY custom payment method without requiring
  4. //! method-specific validation or request parsing.
  5. //!
  6. //! Special handling for bolt11 and bolt12:
  7. //! When the method parameter is "bolt11" or "bolt12", these handlers use the
  8. //! specific Bolt11/Bolt12 request/response types instead of the generic custom types.
  9. use axum::extract::{FromRequestParts, Json, Path, State};
  10. use axum::http::request::Parts;
  11. use axum::http::StatusCode;
  12. use axum::response::{IntoResponse, Response};
  13. use cdk::mint::QuoteId;
  14. #[cfg(feature = "auth")]
  15. use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
  16. use cdk::nuts::{
  17. MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request,
  18. MeltQuoteCustomRequest, MintQuoteBolt11Request, MintQuoteBolt11Response,
  19. MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteCustomRequest, MintRequest,
  20. MintResponse,
  21. };
  22. use serde_json::Value;
  23. use tracing::instrument;
  24. #[cfg(feature = "auth")]
  25. use crate::auth::AuthHeader;
  26. use crate::router_handlers::into_response;
  27. use crate::MintState;
  28. const PREFER_HEADER_KEY: &str = "Prefer";
  29. /// Header extractor for the Prefer header
  30. ///
  31. /// This extractor checks for the `Prefer: respond-async` header
  32. /// to determine if the client wants asynchronous processing
  33. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  34. pub struct PreferHeader {
  35. pub respond_async: bool,
  36. }
  37. impl<S> FromRequestParts<S> for PreferHeader
  38. where
  39. S: Send + Sync,
  40. {
  41. type Rejection = (StatusCode, String);
  42. async fn from_request_parts(
  43. parts: &mut Parts,
  44. _state: &S,
  45. ) -> anyhow::Result<Self, Self::Rejection> {
  46. // Check for Prefer header
  47. if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
  48. let value = prefer_value.to_str().map_err(|_| {
  49. (
  50. StatusCode::BAD_REQUEST,
  51. "Invalid Prefer header value".to_string(),
  52. )
  53. })?;
  54. // Check if it contains "respond-async"
  55. let respond_async = value.to_lowercase().contains("respond-async");
  56. return Ok(PreferHeader { respond_async });
  57. }
  58. // No Prefer header found - default to synchronous processing
  59. Ok(PreferHeader {
  60. respond_async: false,
  61. })
  62. }
  63. }
  64. /// Generic handler for custom payment method mint quotes
  65. ///
  66. /// This handler works for ANY custom payment method (e.g., paypal, venmo, cashapp, bolt11, bolt12).
  67. /// For bolt11/bolt12, it handles the specific request/response types.
  68. /// For other methods, it passes the request data directly to the payment processor.
  69. #[instrument(skip_all, fields(method = ?method))]
  70. pub async fn post_mint_custom_quote(
  71. #[cfg(feature = "auth")] auth: AuthHeader,
  72. State(state): State<MintState>,
  73. Path(method): Path<String>,
  74. Json(payload): Json<Value>,
  75. ) -> Result<Response, Response> {
  76. #[cfg(feature = "auth")]
  77. {
  78. state
  79. .mint
  80. .verify_auth(
  81. auth.into(),
  82. &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote(method.clone())),
  83. )
  84. .await
  85. .map_err(into_response)?;
  86. }
  87. match method.as_str() {
  88. "bolt11" => {
  89. let bolt11_request: MintQuoteBolt11Request =
  90. serde_json::from_value(payload).map_err(|e| {
  91. tracing::error!("Failed to parse bolt11 request: {}", e);
  92. into_response(cdk::Error::InvalidPaymentMethod)
  93. })?;
  94. let quote = state
  95. .mint
  96. .get_mint_quote(bolt11_request.into())
  97. .await
  98. .map_err(into_response)?;
  99. let response: MintQuoteBolt11Response<QuoteId> =
  100. quote.try_into().map_err(into_response)?;
  101. Ok(Json(response).into_response())
  102. }
  103. "bolt12" => {
  104. let bolt12_request: MintQuoteBolt12Request =
  105. serde_json::from_value(payload).map_err(|e| {
  106. tracing::error!("Failed to parse bolt12 request: {}", e);
  107. into_response(cdk::Error::InvalidPaymentMethod)
  108. })?;
  109. let quote = state
  110. .mint
  111. .get_mint_quote(bolt12_request.into())
  112. .await
  113. .map_err(into_response)?;
  114. let response: MintQuoteBolt12Response<QuoteId> =
  115. quote.try_into().map_err(into_response)?;
  116. Ok(Json(response).into_response())
  117. }
  118. _ => {
  119. let custom_request: MintQuoteCustomRequest =
  120. serde_json::from_value(payload).map_err(|e| {
  121. tracing::error!("Failed to parse custom request: {}", e);
  122. into_response(cdk::Error::InvalidPaymentMethod)
  123. })?;
  124. let quote_request = cdk::mint::MintQuoteRequest::Custom {
  125. method,
  126. request: custom_request,
  127. };
  128. let response = state
  129. .mint
  130. .get_mint_quote(quote_request)
  131. .await
  132. .map_err(into_response)?;
  133. match response {
  134. cdk::mint::MintQuoteResponse::Custom { response, .. } => {
  135. Ok(Json(response).into_response())
  136. }
  137. _ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
  138. }
  139. }
  140. }
  141. }
  142. /// Get custom payment method mint quote status
  143. #[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
  144. pub async fn get_check_mint_custom_quote(
  145. #[cfg(feature = "auth")] auth: AuthHeader,
  146. State(state): State<MintState>,
  147. Path((method, quote_id)): Path<(String, QuoteId)>,
  148. ) -> Result<Response, Response> {
  149. #[cfg(feature = "auth")]
  150. {
  151. state
  152. .mint
  153. .verify_auth(
  154. auth.into(),
  155. &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuote(method.clone())),
  156. )
  157. .await
  158. .map_err(into_response)?;
  159. }
  160. let quote_response = state
  161. .mint
  162. .check_mint_quote(&quote_id)
  163. .await
  164. .map_err(into_response)?;
  165. match method.as_str() {
  166. "bolt11" => {
  167. let response: MintQuoteBolt11Response<QuoteId> =
  168. quote_response.try_into().map_err(into_response)?;
  169. Ok(Json(response).into_response())
  170. }
  171. "bolt12" => {
  172. let response: MintQuoteBolt12Response<QuoteId> =
  173. quote_response.try_into().map_err(into_response)?;
  174. Ok(Json(response).into_response())
  175. }
  176. _ => {
  177. // Extract and verify it's a Custom payment method
  178. match quote_response {
  179. cdk::mint::MintQuoteResponse::Custom {
  180. method: quote_method,
  181. response,
  182. } => {
  183. if quote_method != method {
  184. return Err(into_response(cdk::Error::InvalidPaymentMethod));
  185. }
  186. Ok(Json(response).into_response())
  187. }
  188. _ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
  189. }
  190. }
  191. }
  192. }
  193. /// Mint tokens with custom payment method
  194. #[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
  195. pub async fn post_mint_custom(
  196. #[cfg(feature = "auth")] auth: AuthHeader,
  197. State(state): State<MintState>,
  198. Path(method): Path<String>,
  199. Json(payload): Json<MintRequest<QuoteId>>,
  200. ) -> Result<Json<MintResponse>, Response> {
  201. #[cfg(feature = "auth")]
  202. {
  203. state
  204. .mint
  205. .verify_auth(
  206. auth.into(),
  207. &ProtectedEndpoint::new(Method::Post, RoutePath::Mint(method.clone())),
  208. )
  209. .await
  210. .map_err(into_response)?;
  211. }
  212. // Note: process_mint_request will validate the quote internally
  213. // including checking if it's paid and matches the expected payment method
  214. let res = state
  215. .mint
  216. .process_mint_request(payload)
  217. .await
  218. .map_err(into_response)?;
  219. Ok(Json(res))
  220. }
  221. /// Request a melt quote for custom payment method
  222. #[instrument(skip_all, fields(method = ?method))]
  223. pub async fn post_melt_custom_quote(
  224. #[cfg(feature = "auth")] auth: AuthHeader,
  225. State(state): State<MintState>,
  226. Path(method): Path<String>,
  227. Json(payload): Json<Value>,
  228. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  229. #[cfg(feature = "auth")]
  230. {
  231. state
  232. .mint
  233. .verify_auth(
  234. auth.into(),
  235. &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuote(method.clone())),
  236. )
  237. .await
  238. .map_err(into_response)?;
  239. }
  240. let response = match method.as_str() {
  241. "bolt11" => {
  242. let bolt11_request: MeltQuoteBolt11Request =
  243. serde_json::from_value(payload).map_err(|e| {
  244. tracing::error!("Failed to parse bolt11 melt request: {}", e);
  245. into_response(cdk::Error::InvalidPaymentMethod)
  246. })?;
  247. state
  248. .mint
  249. .get_melt_quote(bolt11_request.into())
  250. .await
  251. .map_err(into_response)?
  252. }
  253. "bolt12" => {
  254. let bolt12_request: MeltQuoteBolt12Request =
  255. serde_json::from_value(payload).map_err(|e| {
  256. tracing::error!("Failed to parse bolt12 melt request: {}", e);
  257. into_response(cdk::Error::InvalidPaymentMethod)
  258. })?;
  259. state
  260. .mint
  261. .get_melt_quote(bolt12_request.into())
  262. .await
  263. .map_err(into_response)?
  264. }
  265. _ => {
  266. let custom_request: MeltQuoteCustomRequest =
  267. serde_json::from_value(payload).map_err(|e| {
  268. tracing::error!("Failed to parse custom melt request: {}", e);
  269. into_response(cdk::Error::InvalidPaymentMethod)
  270. })?;
  271. state
  272. .mint
  273. .get_melt_quote(custom_request.into())
  274. .await
  275. .map_err(into_response)?
  276. }
  277. };
  278. Ok(Json(response))
  279. }
  280. /// Get custom payment method melt quote status
  281. #[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
  282. pub async fn get_check_melt_custom_quote(
  283. #[cfg(feature = "auth")] auth: AuthHeader,
  284. State(state): State<MintState>,
  285. Path((method, quote_id)): Path<(String, QuoteId)>,
  286. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  287. #[cfg(feature = "auth")]
  288. {
  289. state
  290. .mint
  291. .verify_auth(
  292. auth.into(),
  293. &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuote(method.clone())),
  294. )
  295. .await
  296. .map_err(into_response)?;
  297. }
  298. // Note: check_melt_quote returns the response directly
  299. // The payment method validation is done when the quote was created
  300. let quote = state
  301. .mint
  302. .check_melt_quote(&quote_id)
  303. .await
  304. .map_err(into_response)?;
  305. Ok(Json(quote))
  306. }
  307. /// Melt tokens with custom payment method
  308. #[instrument(skip_all, fields(method = ?method))]
  309. pub async fn post_melt_custom(
  310. #[cfg(feature = "auth")] auth: AuthHeader,
  311. prefer: PreferHeader,
  312. State(state): State<MintState>,
  313. Path(method): Path<String>,
  314. Json(payload): Json<cdk::nuts::MeltRequest<QuoteId>>,
  315. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  316. #[cfg(feature = "auth")]
  317. {
  318. state
  319. .mint
  320. .verify_auth(
  321. auth.into(),
  322. &ProtectedEndpoint::new(Method::Post, RoutePath::Melt(method.clone())),
  323. )
  324. .await
  325. .map_err(into_response)?;
  326. }
  327. let res = if prefer.respond_async {
  328. // Asynchronous processing - return immediately after setup
  329. state
  330. .mint
  331. .melt_async(&payload)
  332. .await
  333. .map_err(into_response)?
  334. } else {
  335. // Synchronous processing - wait for completion
  336. state.mint.melt(&payload).await.map_err(into_response)?
  337. };
  338. Ok(Json(res))
  339. }
  340. // ============================================================================
  341. // CACHED HANDLERS FOR NUT-19 SUPPORT
  342. // ============================================================================
  343. /// Cached version of post_mint_custom for NUT-19 caching support
  344. #[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
  345. pub async fn cache_post_mint_custom(
  346. #[cfg(feature = "auth")] auth: AuthHeader,
  347. state: State<MintState>,
  348. method: Path<String>,
  349. payload: Json<MintRequest<QuoteId>>,
  350. ) -> Result<Json<MintResponse>, Response> {
  351. use std::ops::Deref;
  352. let State(mint_state) = state.clone();
  353. let json_extracted_payload = payload.deref();
  354. let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
  355. Some(key) => key,
  356. None => {
  357. // Could not calculate key, just return the handler result
  358. #[cfg(feature = "auth")]
  359. return post_mint_custom(auth, state, method, payload).await;
  360. #[cfg(not(feature = "auth"))]
  361. return post_mint_custom(state, method, payload).await;
  362. }
  363. };
  364. if let Some(cached_response) = mint_state.cache.get::<MintResponse>(&cache_key).await {
  365. return Ok(Json(cached_response));
  366. }
  367. #[cfg(feature = "auth")]
  368. let result = post_mint_custom(auth, state, method, payload).await?;
  369. #[cfg(not(feature = "auth"))]
  370. let result = post_mint_custom(state, method, payload).await?;
  371. // Cache the response
  372. mint_state.cache.set(cache_key, result.deref()).await;
  373. Ok(result)
  374. }
  375. #[cfg(test)]
  376. mod tests {
  377. use axum::http::{HeaderValue, Request, StatusCode};
  378. use super::*;
  379. fn create_test_request(prefer_header: Option<&str>) -> Request<()> {
  380. let mut req = Request::builder()
  381. .method("POST")
  382. .uri("/test")
  383. .body(())
  384. .unwrap();
  385. if let Some(header_value) = prefer_header {
  386. req.headers_mut().insert(
  387. PREFER_HEADER_KEY,
  388. HeaderValue::from_str(header_value).unwrap(),
  389. );
  390. }
  391. req
  392. }
  393. fn create_test_request_with_bytes(bytes: &[u8]) -> Request<()> {
  394. let mut req = Request::builder()
  395. .method("POST")
  396. .uri("/test")
  397. .body(())
  398. .unwrap();
  399. req.headers_mut()
  400. .insert(PREFER_HEADER_KEY, HeaderValue::from_bytes(bytes).unwrap());
  401. req
  402. }
  403. #[tokio::test]
  404. async fn test_prefer_header_respond_async() {
  405. let req = create_test_request(Some("respond-async"));
  406. let (mut parts, _) = req.into_parts();
  407. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  408. assert!(result.is_ok());
  409. assert!(result.unwrap().respond_async);
  410. }
  411. #[tokio::test]
  412. async fn test_prefer_header_respond_async_with_other_values() {
  413. let req = create_test_request(Some("respond-async; wait=10"));
  414. let (mut parts, _) = req.into_parts();
  415. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  416. assert!(result.is_ok());
  417. assert!(result.unwrap().respond_async);
  418. }
  419. #[tokio::test]
  420. async fn test_prefer_header_case_insensitive() {
  421. let req = create_test_request(Some("RESPOND-ASYNC"));
  422. let (mut parts, _) = req.into_parts();
  423. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  424. assert!(result.is_ok());
  425. assert!(result.unwrap().respond_async);
  426. }
  427. #[tokio::test]
  428. async fn test_prefer_header_no_respond_async() {
  429. let req = create_test_request(Some("wait=10"));
  430. let (mut parts, _) = req.into_parts();
  431. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  432. assert!(result.is_ok());
  433. assert!(!result.unwrap().respond_async);
  434. }
  435. #[tokio::test]
  436. async fn test_prefer_header_missing() {
  437. let req = create_test_request(None);
  438. let (mut parts, _) = req.into_parts();
  439. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  440. assert!(result.is_ok());
  441. assert!(!result.unwrap().respond_async);
  442. }
  443. #[tokio::test]
  444. async fn test_prefer_header_invalid_value() {
  445. let req = create_test_request_with_bytes(&[0xFF, 0xFE]);
  446. let (mut parts, _) = req.into_parts();
  447. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  448. assert!(result.is_err());
  449. let (status, message) = result.unwrap_err();
  450. assert_eq!(status, StatusCode::BAD_REQUEST);
  451. assert_eq!(message, "Invalid Prefer header value");
  452. }
  453. #[tokio::test]
  454. async fn test_prefer_header_empty_value() {
  455. let req = create_test_request(Some(""));
  456. let (mut parts, _) = req.into_parts();
  457. let result = PreferHeader::from_request_parts(&mut parts, &()).await;
  458. assert!(result.is_ok());
  459. assert!(!result.unwrap().respond_async);
  460. }
  461. }
  462. /// Cached version of post_melt_custom for NUT-19 caching support
  463. #[instrument(skip_all, fields(method = ?method))]
  464. pub async fn cache_post_melt_custom(
  465. #[cfg(feature = "auth")] auth: AuthHeader,
  466. prefer: PreferHeader,
  467. state: State<MintState>,
  468. method: Path<String>,
  469. payload: Json<cdk::nuts::MeltRequest<QuoteId>>,
  470. ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
  471. use std::ops::Deref;
  472. let State(mint_state) = state.clone();
  473. let json_extracted_payload = payload.deref();
  474. let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
  475. Some(key) => key,
  476. None => {
  477. // Could not calculate key, just return the handler result
  478. #[cfg(feature = "auth")]
  479. return post_melt_custom(auth, prefer, state, method, payload).await;
  480. #[cfg(not(feature = "auth"))]
  481. return post_melt_custom(prefer, state, method, payload).await;
  482. }
  483. };
  484. if let Some(cached_response) = mint_state
  485. .cache
  486. .get::<MeltQuoteBolt11Response<QuoteId>>(&cache_key)
  487. .await
  488. {
  489. return Ok(Json(cached_response));
  490. }
  491. #[cfg(feature = "auth")]
  492. let result = post_melt_custom(auth, prefer, state, method, payload).await?;
  493. #[cfg(not(feature = "auth"))]
  494. let result = post_melt_custom(prefer, state, method, payload).await?;
  495. // Cache the response
  496. mint_state.cache.set(cache_key, result.deref()).await;
  497. Ok(result)
  498. }