diff --git a/crates/cashu/examples/payment_request_encoding_benchmark.rs b/crates/cashu/examples/payment_request_encoding_benchmark.rs index 38100e1b31..c14a0c39f0 100644 --- a/crates/cashu/examples/payment_request_encoding_benchmark.rs +++ b/crates/cashu/examples/payment_request_encoding_benchmark.rs @@ -94,6 +94,9 @@ fn minimal_comparison() -> Result<(), Box> { unit: None, single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com")?], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -110,6 +113,9 @@ fn amount_unit_comparison() -> Result<(), Box> { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com")?], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -131,6 +137,9 @@ fn multiple_mints_comparison() -> Result<(), Box> { MintUrl::from_str("https://mint3.example.com")?, MintUrl::from_str("https://backup-mint.cashu.space")?, ], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Payment with multiple mint options".to_string()), transports: vec![], nut10: None, @@ -156,6 +165,9 @@ fn transport_comparison() -> Result<(), Box> { unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: vec![MintUrl::from_str("https://mint.example.com")?], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Payment with callback transport".to_string()), transports: vec![transport], nut10: None, @@ -193,6 +205,9 @@ fn complete_with_nut10_comparison() -> Result<(), Box> { MintUrl::from_str("https://mint1.example.com")?, MintUrl::from_str("https://mint2.example.com")?, ], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Complete payment with P2PK locking and refund key".to_string()), transports: vec![transport], nut10: Some(nut10), @@ -245,6 +260,9 @@ fn very_complex_comparison() -> Result<(), Box> { MintUrl::from_str("https://backup-mint-2.example.net")?, MintUrl::from_str("https://emergency-mint.example.io")?, ], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Complex payment with multiple mints and transports".to_string()), transports: vec![transport1, transport2], nut10: Some(nut10), @@ -503,6 +521,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Test".to_string()), transports: vec![], nut10: None, diff --git a/crates/cashu/src/nuts/nut18/payment_request.rs b/crates/cashu/src/nuts/nut18/payment_request.rs index 8964b3fa08..07eee0a693 100644 --- a/crates/cashu/src/nuts/nut18/payment_request.rs +++ b/crates/cashu/src/nuts/nut18/payment_request.rs @@ -36,6 +36,18 @@ pub struct PaymentRequest { #[serde(rename = "m")] #[serde(skip_serializing_if = "Vec::is_empty", default)] pub mints: Vec, + /// Mints strict flag + #[serde(rename = "ms")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mints_strict: Option, + /// Additional fee reserve for payments from non-preferred mints + #[serde(rename = "fr")] + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_reserve: Option, + /// Supported payment methods the mint must support + #[serde(rename = "sm")] + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub supported_methods: Vec, /// Description #[serde(rename = "d")] pub description: Option, @@ -102,6 +114,9 @@ pub struct PaymentRequestBuilder { unit: Option, single_use: Option, mints: Vec, + mints_strict: Option, + fee_reserve: Option, + supported_methods: Vec, description: Option, transports: Vec, nut10: Option, @@ -150,6 +165,27 @@ impl PaymentRequestBuilder { self } + /// Set mints strict flag + pub fn mints_strict(mut self, mints_strict: bool) -> Self { + self.mints_strict = Some(mints_strict); + self + } + + /// Set fee reserve for payments from non-preferred mints + pub fn fee_reserve(mut self, fee_reserve: A) -> Self + where + A: Into, + { + self.fee_reserve = Some(fee_reserve.into()); + self + } + + /// Set supported payment methods + pub fn supported_methods(mut self, methods: Vec) -> Self { + self.supported_methods = methods; + self + } + /// Set description pub fn description>(mut self, description: S) -> Self { self.description = Some(description.into()); @@ -182,6 +218,9 @@ impl PaymentRequestBuilder { unit: self.unit, single_use: self.single_use, mints: self.mints, + mints_strict: self.mints_strict, + fee_reserve: self.fee_reserve, + supported_methods: self.supported_methods, description: self.description, transports: self.transports, nut10: self.nut10, @@ -249,6 +288,9 @@ mod tests { mints: vec!["https://nofees.testnut.cashu.space" .parse() .expect("valid mint url")], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![transport.clone()], nut10: None, @@ -693,6 +735,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Test both formats".to_string()), transports: vec![], nut10: None, diff --git a/crates/cashu/src/nuts/nut26/encoding.rs b/crates/cashu/src/nuts/nut26/encoding.rs index e18c704f92..9885a17956 100644 --- a/crates/cashu/src/nuts/nut26/encoding.rs +++ b/crates/cashu/src/nuts/nut26/encoding.rs @@ -145,6 +145,9 @@ impl PaymentRequest { /// unit: Some(cashu::nuts::CurrencyUnit::Sat), /// single_use: None, /// mints: vec![MintUrl::from_str("https://mint.example.com")?], + /// mints_strict: None, + /// fee_reserve: None, + /// supported_methods: vec![], /// description: None, /// transports: vec![], /// nut10: None, @@ -221,6 +224,9 @@ impl PaymentRequest { let mut unit: Option = None; let mut single_use: Option = None; let mut mints: Vec = Vec::new(); + let mut mints_strict: Option = None; + let mut fee_reserve: Option = None; + let mut supported_methods: Vec = Vec::new(); let mut description: Option = None; let mut transports: Vec = Vec::new(); let mut nut10: Option = None; @@ -292,6 +298,34 @@ impl PaymentRequest { // nut10: sub-TLV nut10 = Some(Self::decode_nut10(&value)?); } + 0x09 => { + // mint_strict: u8 (0 or 1) + if mints_strict.is_some() { + return Err(Error::InvalidStructure); + } + if !value.is_empty() { + mints_strict = Some(value[0] != 0); + } + } + 0x0a => { + // fee_reserve: u64 + if fee_reserve.is_some() { + return Err(Error::InvalidStructure); + } + if value.len() != 8 { + return Err(Error::InvalidLength); + } + let fr_val = u64::from_be_bytes([ + value[0], value[1], value[2], value[3], value[4], value[5], value[6], + value[7], + ]); + fee_reserve = Some(Amount::from(fr_val)); + } + 0x0b => { + // supported_methods: string (repeatable) + let method = String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?; + supported_methods.push(method); + } _ => { // Unknown tags are ignored } @@ -304,6 +338,9 @@ impl PaymentRequest { unit, single_use, mints, + mints_strict, + fee_reserve, + supported_methods, description, transports, nut10, @@ -362,6 +399,21 @@ impl PaymentRequest { writer.write_tlv(0x08, &nut10_bytes); } + // 0x09 mint_strict: u8 (0 or 1) + if let Some(mints_strict) = self.mints_strict { + writer.write_tlv(0x09, &[if mints_strict { 1 } else { 0 }]); + } + + // 0x0a fee_reserve: u64 + if let Some(fee_reserve) = self.fee_reserve { + writer.write_tlv(0x0a, &fee_reserve.to_u64().to_be_bytes()); + } + + // 0x0b supported_methods: string (repeatable) + for method in &self.supported_methods { + writer.write_tlv(0x0b, method.as_bytes()); + } + Ok(writer.into_bytes()) } @@ -787,6 +839,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Test payment".to_string()), transports: vec![transport], nut10: None, @@ -816,6 +871,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -844,6 +902,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("P2PK locked payment".to_string()), transports: vec![], nut10: Some(nut10.clone()), @@ -866,6 +927,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -911,6 +975,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -930,6 +997,9 @@ mod tests { unit: Some(CurrencyUnit::Usd), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -1106,6 +1176,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Nostr payment".to_string()), transports: vec![transport], nut10: None, @@ -1150,6 +1223,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Nostr payment with relays".to_string()), transports: vec![transport], nut10: None, @@ -1197,6 +1273,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Coffee".to_string()), transports: vec![transport], nut10: None, @@ -1273,6 +1352,9 @@ mod tests { MintUrl::from_str("https://mint2.example.com").unwrap(), MintUrl::from_str("https://testnut.cashu.space").unwrap(), ], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Payment with multiple transports and mints".to_string()), transports: vec![transport1, transport2], nut10: None, @@ -1767,6 +1849,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: Some("Test payment description".to_string()), transports: vec![], nut10: None, @@ -1802,6 +1887,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: Some(true), mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -1832,6 +1920,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: Some(false), mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -1862,6 +1953,9 @@ mod tests { unit: Some(CurrencyUnit::Msat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -1893,6 +1987,9 @@ mod tests { unit: Some(CurrencyUnit::Usd), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -2087,6 +2184,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], // Empty transports = in-band per NUT-26 nut10: None, @@ -2186,6 +2286,9 @@ mod tests { unit: Some(CurrencyUnit::Sat), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, @@ -2223,6 +2326,9 @@ mod tests { unit: Some(CurrencyUnit::Custom("btc".to_string())), single_use: None, mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()], + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: None, transports: vec![], nut10: None, diff --git a/crates/cdk-ffi/src/types/payment_request.rs b/crates/cdk-ffi/src/types/payment_request.rs index 4623dead94..16a9dfbe76 100644 --- a/crates/cdk-ffi/src/types/payment_request.rs +++ b/crates/cdk-ffi/src/types/payment_request.rs @@ -159,6 +159,21 @@ impl PaymentRequest { self.inner.mints.iter().map(|m| m.to_string()).collect() } + /// Get whether the mint list is strict + pub fn mints_strict(&self) -> Option { + self.inner.mints_strict + } + + /// Get the fee reserve for payments from non-preferred mints + pub fn fee_reserve(&self) -> Option { + self.inner.fee_reserve.map(|a| a.into()) + } + + /// Get the list of supported payment methods the mint must support + pub fn supported_methods(&self) -> Vec { + self.inner.supported_methods.clone() + } + /// Get the description pub fn description(&self) -> Option { self.inner.description.clone() diff --git a/crates/cdk/src/wallet/payment_request.rs b/crates/cdk/src/wallet/payment_request.rs index 4cccf10bcb..6b1ec62a45 100644 --- a/crates/cdk/src/wallet/payment_request.rs +++ b/crates/cdk/src/wallet/payment_request.rs @@ -546,6 +546,9 @@ impl WalletRepository { unit: Some(CurrencyUnit::from_str(¶ms.unit)?), single_use: Some(true), mints, + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: params.description, transports, nut10, @@ -615,6 +618,9 @@ impl WalletRepository { unit: Some(CurrencyUnit::from_str(¶ms.unit)?), single_use: Some(true), mints, + mints_strict: None, + fee_reserve: None, + supported_methods: vec![], description: params.description, transports, nut10, diff --git a/fuzz/src/arbitrary_ext.rs b/fuzz/src/arbitrary_ext.rs index e96879969b..1f16f6d449 100644 --- a/fuzz/src/arbitrary_ext.rs +++ b/fuzz/src/arbitrary_ext.rs @@ -623,6 +623,9 @@ impl<'a> Arbitrary<'a> for PaymentRequestArb { unit, single_use, mints, + mints_strict: None, + fee_reserve: None, + supported_methods: Vec::new(), description, transports: Vec::new(), nut10: None,