| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- //! Dynamic router creation for custom payment methods
- //!
- //! Creates dedicated routes for each configured custom payment method,
- //! matching the URL pattern of bolt11/bolt12 routes (e.g., /v1/mint/quote/paypal).
- use axum::routing::{get, post};
- use axum::Router;
- use crate::custom_handlers::{
- cache_post_melt_custom, cache_post_mint_custom, get_check_melt_custom_quote,
- get_check_mint_custom_quote, post_melt_custom_quote, post_mint_custom_quote,
- };
- use crate::MintState;
- /// Creates routers for all configured custom payment methods
- ///
- /// Creates a single set of parameterized routes that handle all custom methods:
- /// - `/mint/quote/{method}` - POST: Create mint quote
- /// - `/mint/quote/{method}/{quote_id}` - GET: Check mint quote status
- /// - `/mint/{method}` - POST: Mint tokens
- /// - `/melt/quote/{method}` - POST: Create melt quote
- /// - `/melt/quote/{method}/{quote_id}` - GET: Check melt quote status
- /// - `/melt/{method}` - POST: Melt tokens
- ///
- /// The {method} parameter captures the payment method name dynamically.
- pub fn create_custom_routers(state: MintState, custom_methods: Vec<String>) -> Router<MintState> {
- tracing::info!(
- "Creating routes for {} custom payment methods: {:?}",
- custom_methods.len(),
- custom_methods
- );
- // Create a single router with parameterized routes that handle all custom methods
- // Use cached versions for mint/melt to support NUT-19 caching
- Router::new()
- .route("/mint/quote/{method}", post(post_mint_custom_quote))
- .route(
- "/mint/quote/{method}/{quote_id}",
- get(get_check_mint_custom_quote),
- )
- .route("/mint/{method}", post(cache_post_mint_custom))
- .route("/melt/quote/{method}", post(post_melt_custom_quote))
- .route(
- "/melt/quote/{method}/{quote_id}",
- get(get_check_melt_custom_quote),
- )
- .route("/melt/{method}", post(cache_post_melt_custom))
- .with_state(state)
- }
- /// Validates that custom method names are valid
- ///
- /// Previously, bolt11 and bolt12 were reserved, but now they can be handled
- /// through the custom router if the payment processor supports them.
- pub fn validate_custom_method_names(methods: &[String]) -> Result<(), String> {
- for method in methods {
- // Validate method name contains only URL-safe characters
- if !method
- .chars()
- .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
- {
- return Err(format!(
- "Custom payment method name '{}' contains invalid characters. Only alphanumeric, '-', and '_' are allowed.",
- method
- ));
- }
- // Validate method name is not empty
- if method.is_empty() {
- return Err("Custom payment method name cannot be empty".to_string());
- }
- }
- Ok(())
- }
- #[cfg(test)]
- mod tests {
- use cdk::nuts::nut00::KnownMethod;
- use cdk::nuts::PaymentMethod;
- use super::*;
- #[test]
- fn test_validate_custom_method_names_valid() {
- assert!(validate_custom_method_names(&["paypal".to_string()]).is_ok());
- assert!(
- validate_custom_method_names(&["venmo".to_string(), "cashapp".to_string()]).is_ok()
- );
- assert!(validate_custom_method_names(&["my-method".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["my_method".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["method123".to_string()]).is_ok());
- }
- #[test]
- fn test_validate_custom_method_names_bolt11_bolt12_allowed() {
- // bolt11 and bolt12 are now allowed as custom methods
- assert!(validate_custom_method_names(&[
- PaymentMethod::Known(KnownMethod::Bolt11).to_string()
- ])
- .is_ok());
- assert!(validate_custom_method_names(&[
- PaymentMethod::Known(KnownMethod::Bolt12).to_string()
- ])
- .is_ok());
- assert!(validate_custom_method_names(&[
- "paypal".to_string(),
- PaymentMethod::Known(KnownMethod::Bolt11).to_string()
- ])
- .is_ok());
- }
- #[test]
- fn test_validate_custom_method_names_invalid_chars() {
- assert!(validate_custom_method_names(&["pay/pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay@pal".to_string()]).is_err());
- }
- #[test]
- fn test_validate_custom_method_names_empty() {
- assert!(validate_custom_method_names(&["".to_string()]).is_err());
- }
- #[test]
- fn test_validate_custom_method_names_multiple_invalid() {
- assert!(validate_custom_method_names(&[
- "valid".to_string(),
- "in valid".to_string(),
- "also-valid".to_string()
- ])
- .is_err());
- }
- #[test]
- fn test_validate_custom_method_names_special_chars() {
- // Test various special characters that should fail
- assert!(validate_custom_method_names(&["pay.pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay+pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay$pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay%pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay&pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay*pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay(pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay)pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay=pal".to_string()]).is_err());
- assert!(validate_custom_method_names(&["pay#pal".to_string()]).is_err());
- }
- #[test]
- fn test_validate_custom_method_names_edge_cases() {
- // Single character names
- assert!(validate_custom_method_names(&["a".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["1".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["-".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["_".to_string()]).is_ok());
- // Names with only dashes or underscores
- assert!(validate_custom_method_names(&["---".to_string()]).is_ok());
- assert!(validate_custom_method_names(&["___".to_string()]).is_ok());
- // Long names
- let long_name = "a".repeat(100);
- assert!(validate_custom_method_names(&[long_name]).is_ok());
- }
- #[test]
- fn test_validate_custom_method_names_mixed_valid() {
- assert!(validate_custom_method_names(&[
- "paypal".to_string(),
- "cash-app".to_string(),
- "venmo_pay".to_string(),
- "method123".to_string(),
- "UPPERCASE".to_string(),
- ])
- .is_ok());
- }
- #[test]
- fn test_validate_custom_method_names_error_messages() {
- // Test that error messages are descriptive
- let result = validate_custom_method_names(&["pay/pal".to_string()]);
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(err.contains("pay/pal"));
- assert!(err.contains("invalid characters"));
- let result = validate_custom_method_names(&["".to_string()]);
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(err.contains("empty"));
- }
- #[test]
- fn test_validate_custom_method_names_unicode() {
- // Unicode characters should fail (not ASCII alphanumeric)
- assert!(validate_custom_method_names(&["café".to_string()]).is_err());
- assert!(validate_custom_method_names(&["北京".to_string()]).is_err());
- assert!(validate_custom_method_names(&["🚀".to_string()]).is_err());
- }
- #[test]
- fn test_validate_custom_method_names_empty_list() {
- // Empty list should be valid (no methods to validate)
- assert!(validate_custom_method_names(&[]).is_ok());
- }
- #[test]
- fn test_create_custom_routers_method_list() {
- // This test verifies the method list formatting
- let custom_methods = vec!["paypal".to_string(), "venmo".to_string()];
- let methods_str = custom_methods
- .iter()
- .map(|m| m.as_str())
- .collect::<Vec<_>>()
- .join(", ");
- // Verify the method string is formatted correctly
- assert!(methods_str.contains("paypal"));
- assert!(methods_str.contains("venmo"));
- assert_eq!(methods_str, "paypal, venmo");
- }
- }
|