//! Payment Request Encoding Benchmark //! //! Compares NUT-18 (CBOR/base64) vs NUT-26 (Bech32m) encoding formats across //! various payment request complexities to demonstrate format efficiency tradeoffs. //! //! # Format Overview //! //! ## NUT-18 (creqA prefix) //! - **Binary Encoding**: CBOR (Concise Binary Object Representation) //! - **Text Encoding**: URL-safe base64 //! - **Characteristics**: Compact binary format, case-sensitive //! //! ## NUT-26 (CREQB prefix) //! - **Binary Encoding**: TLV (Type-Length-Value) //! - **Text Encoding**: Bech32m //! - **Characteristics**: QR-optimized, case-insensitive, error detection //! //! # When to Use Each Format //! //! ## Use NUT-26 (CREQB) when: //! - **Minimal requests** (~5 bytes / 7% smaller for simple payment IDs) //! - **QR code display** (100% alphanumeric-compatible vs 99%+) //! - **Error detection is critical** (Bech32m has built-in checksums) //! - **Case-insensitive parsing** needed (URLs, voice transcription) //! - **Visual verification** (human-readable structure) //! //! ## Use NUT-18 (creqA) when: //! - **Complex requests** (~13-163 bytes / 16-19% smaller with more data) //! - **Multiple mints** (~59 bytes / 24% smaller with 4 mints) //! - **Transport callbacks** (~49 bytes / 19% smaller with 1 transport) //! - **NUT-10 locking** (~91 bytes / 17% smaller with P2PK) //! - **Nested structures** (CBOR excels at hierarchical data) //! - **Bandwidth is constrained** (smaller encoded size) //! //! # Benchmark Results Summary //! //! | Scenario | NUT-18 Size | NUT-26 Size | Winner | Savings | //! |----------|-------------|-------------|--------|---------| //! | Minimal payment | 77 bytes | 72 bytes | NUT-26 | 5 bytes (7%) | //! | With amount/unit | 81 bytes | 94 bytes | NUT-18 | 13 bytes (16%) | //! | 4 mints | 249 bytes | 308 bytes | NUT-18 | 59 bytes (24%) | //! | 1 transport | 253 bytes | 302 bytes | NUT-18 | 49 bytes (19%) | //! | Complete + P2PK | 529 bytes | 620 bytes | NUT-18 | 91 bytes (17%) | //! | Very complex | 857 bytes | 1020 bytes | NUT-18 | 163 bytes (19%) | //! //! **Key Insight**: NUT-26 is optimal for simple requests, NUT-18 scales better //! for complex payment requests with multiple mints, transports, or NUT-10 locks. use std::str::FromStr; use cashu::nuts::nut10::Kind; use cashu::nuts::{CurrencyUnit, Nut10SecretRequest, PaymentRequest, Transport, TransportType}; use cashu::{Amount, MintUrl}; fn main() -> Result<(), Box> { println!("=== NUT-18 vs NUT-26 Format Comparison ===\n"); // Example 1: Minimal payment request println!("1. Minimal Payment Request:"); minimal_comparison()?; // Example 2: Payment with amount and unit println!("\n2. Payment with Amount and Unit:"); amount_unit_comparison()?; // Example 3: Complex payment with multiple mints println!("\n3. Complex Payment with Multiple Mints:"); multiple_mints_comparison()?; // Example 4: Payment with transport println!("\n4. Payment with Transport:"); transport_comparison()?; // Example 5: Complete payment with NUT-10 locking println!("\n5. Complete Payment with NUT-10 P2PK Lock:"); complete_with_nut10_comparison()?; // Example 6: Very complex payment request println!("\n6. Very Complex Payment Request:"); very_complex_comparison()?; // Summary println!("\n=== Summary ==="); summary(); println!("\n=== Format Comparison Complete ==="); Ok(()) } fn minimal_comparison() -> Result<(), Box> { let payment_request = PaymentRequest { payment_id: Some("test123".to_string()), amount: None, unit: None, single_use: None, mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]), description: None, transports: vec![], nut10: None, }; compare_formats(&payment_request, "Minimal")?; Ok(()) } fn amount_unit_comparison() -> Result<(), Box> { let payment_request = PaymentRequest { payment_id: Some("pay456".to_string()), amount: Some(Amount::from(2100)), unit: Some(CurrencyUnit::Sat), single_use: None, mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]), description: None, transports: vec![], nut10: None, }; compare_formats(&payment_request, "Amount + Unit")?; Ok(()) } fn multiple_mints_comparison() -> Result<(), Box> { let payment_request = PaymentRequest { payment_id: Some("multi789".to_string()), amount: Some(Amount::from(10000)), unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: Some(vec![ MintUrl::from_str("https://mint1.example.com")?, MintUrl::from_str("https://mint2.example.com")?, MintUrl::from_str("https://mint3.example.com")?, MintUrl::from_str("https://backup-mint.cashu.space")?, ]), description: Some("Payment with multiple mint options".to_string()), transports: vec![], nut10: None, }; compare_formats(&payment_request, "Multiple Mints")?; Ok(()) } fn transport_comparison() -> Result<(), Box> { let transport = Transport { _type: TransportType::HttpPost, target: "https://api.example.com/cashu/payment/callback".to_string(), tags: Some(vec![ vec!["method".to_string(), "POST".to_string()], vec!["auth".to_string(), "bearer".to_string()], ]), }; let payment_request = PaymentRequest { payment_id: Some("transport123".to_string()), amount: Some(Amount::from(5000)), unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]), description: Some("Payment with callback transport".to_string()), transports: vec![transport], nut10: None, }; compare_formats(&payment_request, "With Transport")?; Ok(()) } fn complete_with_nut10_comparison() -> Result<(), Box> { let nut10 = Nut10SecretRequest::new( Kind::P2PK, "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198", Some(vec![ vec!["locktime".to_string(), "1609459200".to_string()], vec![ "refund".to_string(), "03a34d1f4e6d1e7f8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2".to_string(), ], ]), ); let transport = Transport { _type: TransportType::HttpPost, target: "https://callback.example.com/payment".to_string(), tags: Some(vec![vec!["priority".to_string(), "high".to_string()]]), }; let payment_request = PaymentRequest { payment_id: Some("complete789".to_string()), amount: Some(Amount::from(5000)), unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: Some(vec![ MintUrl::from_str("https://mint1.example.com")?, MintUrl::from_str("https://mint2.example.com")?, ]), description: Some("Complete payment with P2PK locking and refund key".to_string()), transports: vec![transport], nut10: Some(nut10), }; compare_formats(&payment_request, "Complete with NUT-10")?; Ok(()) } fn very_complex_comparison() -> Result<(), Box> { let nut10 = Nut10SecretRequest::new( Kind::P2PK, "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198", Some(vec![ vec!["locktime".to_string(), "1609459200".to_string()], vec![ "refund".to_string(), "03a34d1f4e6d1e7f8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e".to_string(), ], ]), ); let transport1 = Transport { _type: TransportType::HttpPost, target: "https://primary-callback.example.com/payment/webhook".to_string(), tags: Some(vec![ vec!["priority".to_string(), "high".to_string()], vec!["timeout".to_string(), "30".to_string()], ]), }; let transport2 = Transport { _type: TransportType::HttpPost, target: "https://backup-callback.example.com/payment/webhook".to_string(), tags: Some(vec![ vec!["priority".to_string(), "medium".to_string()], vec!["timeout".to_string(), "60".to_string()], ]), }; let payment_request = PaymentRequest { payment_id: Some("very_complex_payment_id_12345".to_string()), amount: Some(Amount::from(21000)), unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: Some(vec![ MintUrl::from_str("https://primary-mint.cashu.space")?, MintUrl::from_str("https://secondary-mint.example.com")?, MintUrl::from_str("https://backup-mint-1.example.org")?, MintUrl::from_str("https://backup-mint-2.example.net")?, MintUrl::from_str("https://emergency-mint.example.io")?, ]), description: Some("Complex payment with multiple mints and transports".to_string()), transports: vec![transport1, transport2], nut10: Some(nut10), }; compare_formats(&payment_request, "Very Complex")?; Ok(()) } fn compare_formats( payment_request: &PaymentRequest, label: &str, ) -> Result<(), Box> { // Encode using NUT-18 (CBOR/base64, creqA) let nut18_encoded = payment_request.to_string(); // Encode using NUT-26 (Bech32m, CREQB) let nut26_encoded = payment_request.to_bech32_string()?; // Calculate sizes let nut18_size = nut18_encoded.len(); let nut26_size = nut26_encoded.len(); let size_diff = nut26_size as i32 - nut18_size as i32; let size_ratio = (nut26_size as f64 / nut18_size as f64) * 100.0; println!(" {} Payment Request:", label); println!(" Payment ID: {:?}", payment_request.payment_id); println!(" Amount: {:?}", payment_request.amount); println!( " Mints: {}", payment_request.mints.as_ref().map_or(0, |m| m.len()) ); println!(" Transports: {}", payment_request.transports.len()); println!(" NUT-10: {}", payment_request.nut10.is_some()); println!("\n NUT-18 (CBOR/base64, creqA):"); println!(" Size: {} bytes", nut18_size); println!( " Format: {}", &nut18_encoded[..nut18_encoded.len().min(80)] ); if nut18_encoded.len() > 80 { println!(" ... ({} more chars)", nut18_encoded.len() - 80); } println!("\n NUT-26 (Bech32m, CREQB):"); println!(" Size: {} bytes", nut26_size); println!( " Format: {}", &nut26_encoded[..nut26_encoded.len().min(80)] ); if nut26_encoded.len() > 80 { println!(" ... ({} more chars)", nut26_encoded.len() - 80); } println!("\n Comparison:"); println!( " Size difference: {} bytes ({:.1}%)", size_diff, size_ratio ); if size_diff < 0 { println!(" Winner: NUT-26 is {} bytes smaller!", size_diff.abs()); } else if size_diff > 0 { println!(" Winner: NUT-18 is {} bytes smaller!", size_diff); } else { println!(" Equal size!"); } // Analyze QR code efficiency analyze_qr_efficiency(&nut18_encoded, &nut26_encoded); // Verify round-trip for both formats println!("\n Round-trip verification:"); // NUT-18 round-trip let nut18_decoded = PaymentRequest::from_str(&nut18_encoded)?; assert_eq!(nut18_decoded.payment_id, payment_request.payment_id); assert_eq!(nut18_decoded.amount, payment_request.amount); println!(" NUT-18: ✓ Decoded successfully"); // NUT-26 round-trip let nut26_decoded = PaymentRequest::from_str(&nut26_encoded)?; assert_eq!(nut26_decoded.payment_id, payment_request.payment_id); assert_eq!(nut26_decoded.amount, payment_request.amount); println!(" NUT-26: ✓ Decoded successfully"); // Verify both decode to the same data assert_eq!(nut18_decoded.payment_id, nut26_decoded.payment_id); assert_eq!(nut18_decoded.amount, nut26_decoded.amount); assert_eq!(nut18_decoded.unit, nut26_decoded.unit); assert_eq!(nut18_decoded.single_use, nut26_decoded.single_use); assert_eq!(nut18_decoded.description, nut26_decoded.description); println!(" ✓ Both formats decode to identical data"); Ok(()) } fn analyze_qr_efficiency(nut18: &str, nut26: &str) { // QR codes have different encoding modes: // - Alphanumeric: 0-9, A-Z (uppercase), space, $, %, *, +, -, ., /, : (most efficient for text) // - Byte: any data (less efficient) let alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; let nut18_alphanumeric = nut18 .chars() .filter(|c| alphanumeric_chars.contains(c.to_ascii_uppercase())) .count(); let nut18_alphanumeric_ratio = (nut18_alphanumeric as f64 / nut18.len() as f64) * 100.0; let nut26_alphanumeric = nut26 .chars() .filter(|c| alphanumeric_chars.contains(c.to_ascii_uppercase())) .count(); let nut26_alphanumeric_ratio = (nut26_alphanumeric as f64 / nut26.len() as f64) * 100.0; println!("\n QR Code Efficiency:"); println!( " NUT-18: {:.1}% alphanumeric-compatible", nut18_alphanumeric_ratio ); println!( " NUT-26: {:.1}% alphanumeric-compatible", nut26_alphanumeric_ratio ); if nut26_alphanumeric_ratio > nut18_alphanumeric_ratio { println!( " NUT-26 is more QR-friendly (+{:.1}%)", nut26_alphanumeric_ratio - nut18_alphanumeric_ratio ); } // Estimate QR version (simplified) let nut18_qr_version = estimate_qr_version(nut18.len(), nut18_alphanumeric_ratio > 80.0); let nut26_qr_version = estimate_qr_version(nut26.len(), nut26_alphanumeric_ratio > 80.0); println!( " NUT-18 QR version: ~{} ({}×{} modules)", nut18_qr_version, 21 + (nut18_qr_version - 1) * 4, 21 + (nut18_qr_version - 1) * 4 ); println!( " NUT-26 QR version: ~{} ({}×{} modules)", nut26_qr_version, 21 + (nut26_qr_version - 1) * 4, 21 + (nut26_qr_version - 1) * 4 ); } fn estimate_qr_version(data_length: usize, is_alphanumeric: bool) -> u8 { // Simplified QR version estimation (Level L - Low error correction) let capacity = if is_alphanumeric { // Alphanumeric mode capacity match data_length { 0..=20 => 1, 21..=38 => 2, 39..=61 => 3, 62..=90 => 4, 91..=122 => 5, 123..=154 => 6, 155..=192 => 7, 193..=230 => 8, 231..=271 => 9, 272..=321 => 10, 322..=367 => 11, 368..=425 => 12, 426..=458 => 13, 459..=520 => 14, 521..=586 => 15, _ => 16, } } else { // Byte mode capacity match data_length { 0..=14 => 1, 15..=26 => 2, 27..=42 => 3, 43..=62 => 4, 63..=84 => 5, 85..=106 => 6, 107..=122 => 7, 123..=152 => 8, 153..=180 => 9, 181..=213 => 10, 214..=251 => 11, 252..=287 => 12, 288..=331 => 13, 332..=362 => 14, 363..=394 => 15, _ => 16, } }; capacity } fn summary() { println!(" Key Observations:"); println!(" • NUT-18 (creqA): CBOR binary + URL-safe base64 encoding"); println!(" • NUT-26 (CREQB): TLV binary + Bech32m encoding"); println!(" • Bech32m is optimized for QR codes (uppercase alphanumeric)"); println!(" • CBOR may be more compact for complex nested structures"); println!(" • Both formats support the same feature set"); println!(" • NUT-26 has better error detection (Bech32m checksum)"); println!(" • NUT-26 is case-insensitive for parsing"); println!(" • Both can be parsed from the same FromStr implementation"); } #[cfg(test)] mod tests { use super::*; #[test] fn test_minimal_comparison() { assert!(minimal_comparison().is_ok()); } #[test] fn test_amount_unit_comparison() { assert!(amount_unit_comparison().is_ok()); } #[test] fn test_multiple_mints_comparison() { assert!(multiple_mints_comparison().is_ok()); } #[test] fn test_transport_comparison() { assert!(transport_comparison().is_ok()); } #[test] fn test_complete_with_nut10_comparison() { assert!(complete_with_nut10_comparison().is_ok()); } #[test] fn test_very_complex_comparison() { assert!(very_complex_comparison().is_ok()); } #[test] fn test_round_trip_equivalence() { let payment_request = PaymentRequest { payment_id: Some("test".to_string()), amount: Some(Amount::from(1000)), unit: Some(CurrencyUnit::Sat), single_use: None, mints: Some(vec![MintUrl::from_str("https://mint.example.com").unwrap()]), description: Some("Test".to_string()), transports: vec![], nut10: None, }; // Encode both ways let nut18 = payment_request.to_string(); let nut26 = payment_request.to_bech32_string().unwrap(); // Decode both let from_nut18 = PaymentRequest::from_str(&nut18).unwrap(); let from_nut26 = PaymentRequest::from_str(&nut26).unwrap(); // Should be equal assert_eq!(from_nut18.payment_id, from_nut26.payment_id); assert_eq!(from_nut18.amount, from_nut26.amount); assert_eq!(from_nut18.unit, from_nut26.unit); assert_eq!(from_nut18.description, from_nut26.description); } }