custom_router.rs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. //! Dynamic router creation for custom payment methods
  2. //!
  3. //! Creates dedicated routes for each configured custom payment method,
  4. //! matching the URL pattern of bolt11/bolt12 routes (e.g., /v1/mint/quote/paypal).
  5. use axum::routing::{get, post};
  6. use axum::Router;
  7. use crate::custom_handlers::{
  8. cache_post_melt_custom, cache_post_mint_custom, get_check_melt_custom_quote,
  9. get_check_mint_custom_quote, post_melt_custom_quote, post_mint_custom_quote,
  10. };
  11. use crate::MintState;
  12. /// Creates routers for all configured custom payment methods
  13. ///
  14. /// Creates a single set of parameterized routes that handle all custom methods:
  15. /// - `/mint/quote/{method}` - POST: Create mint quote
  16. /// - `/mint/quote/{method}/{quote_id}` - GET: Check mint quote status
  17. /// - `/mint/{method}` - POST: Mint tokens
  18. /// - `/melt/quote/{method}` - POST: Create melt quote
  19. /// - `/melt/quote/{method}/{quote_id}` - GET: Check melt quote status
  20. /// - `/melt/{method}` - POST: Melt tokens
  21. ///
  22. /// The {method} parameter captures the payment method name dynamically.
  23. pub fn create_custom_routers(state: MintState, custom_methods: Vec<String>) -> Router<MintState> {
  24. tracing::info!(
  25. "Creating routes for {} custom payment methods: {:?}",
  26. custom_methods.len(),
  27. custom_methods
  28. );
  29. // Create a single router with parameterized routes that handle all custom methods
  30. // Use cached versions for mint/melt to support NUT-19 caching
  31. Router::new()
  32. .route("/mint/quote/{method}", post(post_mint_custom_quote))
  33. .route(
  34. "/mint/quote/{method}/{quote_id}",
  35. get(get_check_mint_custom_quote),
  36. )
  37. .route("/mint/{method}", post(cache_post_mint_custom))
  38. .route("/melt/quote/{method}", post(post_melt_custom_quote))
  39. .route(
  40. "/melt/quote/{method}/{quote_id}",
  41. get(get_check_melt_custom_quote),
  42. )
  43. .route("/melt/{method}", post(cache_post_melt_custom))
  44. .with_state(state)
  45. }
  46. /// Validates that custom method names are valid
  47. ///
  48. /// Previously, bolt11 and bolt12 were reserved, but now they can be handled
  49. /// through the custom router if the payment processor supports them.
  50. pub fn validate_custom_method_names(methods: &[String]) -> Result<(), String> {
  51. for method in methods {
  52. // Validate method name contains only URL-safe characters
  53. if !method
  54. .chars()
  55. .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
  56. {
  57. return Err(format!(
  58. "Custom payment method name '{}' contains invalid characters. Only alphanumeric, '-', and '_' are allowed.",
  59. method
  60. ));
  61. }
  62. // Validate method name is not empty
  63. if method.is_empty() {
  64. return Err("Custom payment method name cannot be empty".to_string());
  65. }
  66. }
  67. Ok(())
  68. }
  69. #[cfg(test)]
  70. mod tests {
  71. use cdk::nuts::nut00::KnownMethod;
  72. use cdk::nuts::PaymentMethod;
  73. use super::*;
  74. #[test]
  75. fn test_validate_custom_method_names_valid() {
  76. assert!(validate_custom_method_names(&["paypal".to_string()]).is_ok());
  77. assert!(
  78. validate_custom_method_names(&["venmo".to_string(), "cashapp".to_string()]).is_ok()
  79. );
  80. assert!(validate_custom_method_names(&["my-method".to_string()]).is_ok());
  81. assert!(validate_custom_method_names(&["my_method".to_string()]).is_ok());
  82. assert!(validate_custom_method_names(&["method123".to_string()]).is_ok());
  83. }
  84. #[test]
  85. fn test_validate_custom_method_names_bolt11_bolt12_allowed() {
  86. // bolt11 and bolt12 are now allowed as custom methods
  87. assert!(validate_custom_method_names(&[
  88. PaymentMethod::Known(KnownMethod::Bolt11).to_string()
  89. ])
  90. .is_ok());
  91. assert!(validate_custom_method_names(&[
  92. PaymentMethod::Known(KnownMethod::Bolt12).to_string()
  93. ])
  94. .is_ok());
  95. assert!(validate_custom_method_names(&[
  96. "paypal".to_string(),
  97. PaymentMethod::Known(KnownMethod::Bolt11).to_string()
  98. ])
  99. .is_ok());
  100. }
  101. #[test]
  102. fn test_validate_custom_method_names_invalid_chars() {
  103. assert!(validate_custom_method_names(&["pay/pal".to_string()]).is_err());
  104. assert!(validate_custom_method_names(&["pay pal".to_string()]).is_err());
  105. assert!(validate_custom_method_names(&["pay@pal".to_string()]).is_err());
  106. }
  107. #[test]
  108. fn test_validate_custom_method_names_empty() {
  109. assert!(validate_custom_method_names(&["".to_string()]).is_err());
  110. }
  111. #[test]
  112. fn test_validate_custom_method_names_multiple_invalid() {
  113. assert!(validate_custom_method_names(&[
  114. "valid".to_string(),
  115. "in valid".to_string(),
  116. "also-valid".to_string()
  117. ])
  118. .is_err());
  119. }
  120. #[test]
  121. fn test_validate_custom_method_names_special_chars() {
  122. // Test various special characters that should fail
  123. assert!(validate_custom_method_names(&["pay.pal".to_string()]).is_err());
  124. assert!(validate_custom_method_names(&["pay+pal".to_string()]).is_err());
  125. assert!(validate_custom_method_names(&["pay$pal".to_string()]).is_err());
  126. assert!(validate_custom_method_names(&["pay%pal".to_string()]).is_err());
  127. assert!(validate_custom_method_names(&["pay&pal".to_string()]).is_err());
  128. assert!(validate_custom_method_names(&["pay*pal".to_string()]).is_err());
  129. assert!(validate_custom_method_names(&["pay(pal".to_string()]).is_err());
  130. assert!(validate_custom_method_names(&["pay)pal".to_string()]).is_err());
  131. assert!(validate_custom_method_names(&["pay=pal".to_string()]).is_err());
  132. assert!(validate_custom_method_names(&["pay#pal".to_string()]).is_err());
  133. }
  134. #[test]
  135. fn test_validate_custom_method_names_edge_cases() {
  136. // Single character names
  137. assert!(validate_custom_method_names(&["a".to_string()]).is_ok());
  138. assert!(validate_custom_method_names(&["1".to_string()]).is_ok());
  139. assert!(validate_custom_method_names(&["-".to_string()]).is_ok());
  140. assert!(validate_custom_method_names(&["_".to_string()]).is_ok());
  141. // Names with only dashes or underscores
  142. assert!(validate_custom_method_names(&["---".to_string()]).is_ok());
  143. assert!(validate_custom_method_names(&["___".to_string()]).is_ok());
  144. // Long names
  145. let long_name = "a".repeat(100);
  146. assert!(validate_custom_method_names(&[long_name]).is_ok());
  147. }
  148. #[test]
  149. fn test_validate_custom_method_names_mixed_valid() {
  150. assert!(validate_custom_method_names(&[
  151. "paypal".to_string(),
  152. "cash-app".to_string(),
  153. "venmo_pay".to_string(),
  154. "method123".to_string(),
  155. "UPPERCASE".to_string(),
  156. ])
  157. .is_ok());
  158. }
  159. #[test]
  160. fn test_validate_custom_method_names_error_messages() {
  161. // Test that error messages are descriptive
  162. let result = validate_custom_method_names(&["pay/pal".to_string()]);
  163. assert!(result.is_err());
  164. let err = result.unwrap_err();
  165. assert!(err.contains("pay/pal"));
  166. assert!(err.contains("invalid characters"));
  167. let result = validate_custom_method_names(&["".to_string()]);
  168. assert!(result.is_err());
  169. let err = result.unwrap_err();
  170. assert!(err.contains("empty"));
  171. }
  172. #[test]
  173. fn test_validate_custom_method_names_unicode() {
  174. // Unicode characters should fail (not ASCII alphanumeric)
  175. assert!(validate_custom_method_names(&["café".to_string()]).is_err());
  176. assert!(validate_custom_method_names(&["北京".to_string()]).is_err());
  177. assert!(validate_custom_method_names(&["🚀".to_string()]).is_err());
  178. }
  179. #[test]
  180. fn test_validate_custom_method_names_empty_list() {
  181. // Empty list should be valid (no methods to validate)
  182. assert!(validate_custom_method_names(&[]).is_ok());
  183. }
  184. #[test]
  185. fn test_create_custom_routers_method_list() {
  186. // This test verifies the method list formatting
  187. let custom_methods = vec!["paypal".to_string(), "venmo".to_string()];
  188. let methods_str = custom_methods
  189. .iter()
  190. .map(|m| m.as_str())
  191. .collect::<Vec<_>>()
  192. .join(", ");
  193. // Verify the method string is formatted correctly
  194. assert!(methods_str.contains("paypal"));
  195. assert!(methods_str.contains("venmo"));
  196. assert_eq!(methods_str, "paypal, venmo");
  197. }
  198. }