1use std::fmt;
6use std::str::FromStr;
7
8use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
9use bitcoin::base64::{alphabet, Engine};
10use serde::{Deserialize, Serialize};
11
12use super::{Error, Nut10SecretRequest, Transport};
13use crate::mint_url::MintUrl;
14use crate::nut26::CREQ_B_HRP;
15use crate::nuts::{CurrencyUnit, Proofs};
16use crate::Amount;
17
18const PAYMENT_REQUEST_PREFIX: &str = "creqA";
19
20#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PaymentRequest {
23 #[serde(rename = "i")]
25 pub payment_id: Option<String>,
26 #[serde(rename = "a")]
28 pub amount: Option<Amount>,
29 #[serde(rename = "u")]
31 pub unit: Option<CurrencyUnit>,
32 #[serde(rename = "s")]
34 pub single_use: Option<bool>,
35 #[serde(rename = "m")]
37 #[serde(skip_serializing_if = "Vec::is_empty", default)]
38 pub mints: Vec<MintUrl>,
39 #[serde(rename = "d")]
41 pub description: Option<String>,
42 #[serde(rename = "t")]
44 #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")]
45 pub transports: Vec<Transport>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub nut10: Option<Nut10SecretRequest>,
49}
50
51impl PaymentRequest {
52 pub fn builder() -> PaymentRequestBuilder {
54 PaymentRequestBuilder::default()
55 }
56}
57
58impl AsRef<Option<String>> for PaymentRequest {
59 fn as_ref(&self) -> &Option<String> {
60 &self.payment_id
61 }
62}
63
64impl fmt::Display for PaymentRequest {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 use serde::ser::Error;
67 let mut data = Vec::new();
68 ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
69 let encoded = general_purpose::URL_SAFE.encode(data);
70 write!(f, "{PAYMENT_REQUEST_PREFIX}{encoded}")
71 }
72}
73
74impl FromStr for PaymentRequest {
75 type Err = Error;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 if s.to_lowercase().starts_with(CREQ_B_HRP) {
80 return Self::from_bech32_string(s).map_err(Error::Nut26Error);
82 }
83
84 let s = s
86 .strip_prefix(PAYMENT_REQUEST_PREFIX)
87 .ok_or(Error::InvalidPrefix)?;
88
89 let decode_config = general_purpose::GeneralPurposeConfig::new()
90 .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
91 let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
92
93 Ok(ciborium::from_reader(&decoded[..])?)
94 }
95}
96
97#[derive(Debug, Default, Clone)]
99pub struct PaymentRequestBuilder {
100 payment_id: Option<String>,
101 amount: Option<Amount>,
102 unit: Option<CurrencyUnit>,
103 single_use: Option<bool>,
104 mints: Vec<MintUrl>,
105 description: Option<String>,
106 transports: Vec<Transport>,
107 nut10: Option<Nut10SecretRequest>,
108}
109
110impl PaymentRequestBuilder {
111 pub fn payment_id<S>(mut self, payment_id: S) -> Self
113 where
114 S: Into<String>,
115 {
116 self.payment_id = Some(payment_id.into());
117 self
118 }
119
120 pub fn amount<A>(mut self, amount: A) -> Self
122 where
123 A: Into<Amount>,
124 {
125 self.amount = Some(amount.into());
126 self
127 }
128
129 pub fn unit(mut self, unit: CurrencyUnit) -> Self {
131 self.unit = Some(unit);
132 self
133 }
134
135 pub fn single_use(mut self, single_use: bool) -> Self {
137 self.single_use = Some(single_use);
138 self
139 }
140
141 pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
143 self.mints.push(mint_url);
144 self
145 }
146
147 pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
149 self.mints = mints;
150 self
151 }
152
153 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
155 self.description = Some(description.into());
156 self
157 }
158
159 pub fn add_transport(mut self, transport: Transport) -> Self {
161 self.transports.push(transport);
162 self
163 }
164
165 pub fn transports(mut self, transports: Vec<Transport>) -> Self {
167 self.transports = transports;
168 self
169 }
170
171 pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
173 self.nut10 = Some(nut10);
174 self
175 }
176
177 pub fn build(self) -> PaymentRequest {
179 PaymentRequest {
180 payment_id: self.payment_id,
181 amount: self.amount,
182 unit: self.unit,
183 single_use: self.single_use,
184 mints: self.mints,
185 description: self.description,
186 transports: self.transports,
187 nut10: self.nut10,
188 }
189 }
190}
191
192#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
194pub struct PaymentRequestPayload {
195 pub id: Option<String>,
197 pub memo: Option<String>,
199 pub mint: MintUrl,
201 pub unit: CurrencyUnit,
203 pub proofs: Proofs,
205}
206
207#[cfg(test)]
208mod tests {
209 use std::str::FromStr;
210
211 use lightning_invoice::Bolt11Invoice;
212
213 use super::*;
214 use crate::nuts::nut10::Kind;
215 use crate::nuts::SpendingConditions;
216 use crate::TransportType;
217
218 const PAYMENT_REQUEST: &str = "creqAp2FpaGI3YTkwMTc2YWEKYXVjc2F0YXP2YW2BeCJodHRwczovL25vZmVlcy50ZXN0bnV0LmNhc2h1LnNwYWNlYWT2YXSBo2F0ZW5vc3RyYWF4qW5wcm9maWxlMXFxc2dtNnFmYTNjOGR0ejJmdnpodmZxZWFjbXdtMGU1MHBlM2s1dGZtdnBqam1uMHZqN20ydGdwejNtaHh1ZTY5dWhoeWV0dnY5dWp1ZXJwZDQ2aHh0bmZkdXEzd2Ftbnd2YXo3dG1qdjRreHo3Znc4cWVueHZld3dkY3h6Y205OXVxczZhbW53dmF6N3Rtd2RhZWp1bXIwZHM0bGpoN25hZ4GCYW5iMTc=";
219
220 #[test]
221 fn test_decode_payment_req() {
222 let req = PaymentRequest::from_str(PAYMENT_REQUEST).expect("valid payment request");
223
224 assert_eq!(&req.payment_id.unwrap(), "b7a90176");
225 assert_eq!(req.amount.unwrap(), 10.into());
226 assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
227 assert_eq!(
228 req.mints,
229 vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
230 );
231 assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
232
233 let transport = req.transports.first().unwrap();
234
235 let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n".to_string(), tags: vec![vec!["n".to_string(), "17".to_string()]]};
236
237 assert_eq!(transport, &expected_transport);
238 }
239
240 #[test]
241 fn test_roundtrip_payment_req() {
242 let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n".to_string(), tags: vec![vec!["n".to_string(), "17".to_string()]]};
243
244 let request = PaymentRequest {
245 payment_id: Some("b7a90176".to_string()),
246 amount: Some(10.into()),
247 unit: Some(CurrencyUnit::Sat),
248 single_use: None,
249 mints: vec!["https://nofees.testnut.cashu.space"
250 .parse()
251 .expect("valid mint url")],
252 description: None,
253 transports: vec![transport.clone()],
254 nut10: None,
255 };
256
257 let request_str = request.to_string();
258
259 assert_eq!(request_str, PAYMENT_REQUEST);
260
261 let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
262
263 assert_eq!(&req.payment_id.unwrap(), "b7a90176");
264 assert_eq!(req.amount.unwrap(), 10.into());
265 assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
266 assert_eq!(
267 req.mints,
268 vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
269 );
270 assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
271
272 let t = req.transports.first().unwrap();
273 assert_eq!(&transport, t);
274 }
275
276 #[test]
277 fn test_payment_request_builder() {
278 let transport = Transport {
279 _type: TransportType::Nostr,
280 target: "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n".to_string(),
281 tags: vec![vec!["n".to_string(), "17".to_string()]]
282 };
283
284 let mint_url =
285 MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
286
287 let request = PaymentRequest::builder()
289 .payment_id("b7a90176")
290 .amount(Amount::from(10))
291 .unit(CurrencyUnit::Sat)
292 .add_mint(mint_url.clone())
293 .add_transport(transport.clone())
294 .build();
295
296 assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
298 assert_eq!(request.amount.unwrap(), 10.into());
299 assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
300 assert_eq!(request.mints.clone(), vec![mint_url]);
301
302 let t = request.transports.first().unwrap();
303 assert_eq!(&transport, t);
304
305 let request_str = request.to_string();
307 let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
308
309 assert_eq!(req.payment_id, request.payment_id);
310 assert_eq!(req.amount, request.amount);
311 assert_eq!(req.unit, request.unit);
312 }
313
314 #[test]
315 fn test_transport_builder() {
316 let transport = Transport::builder()
318 .transport_type(TransportType::Nostr)
319 .target("nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n")
320 .add_tag(vec!["n".to_string(), "17".to_string()])
321 .build()
322 .expect("Valid transport");
323
324 assert_eq!(transport._type, TransportType::Nostr);
326 assert_eq!(transport.target, "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n");
327 assert_eq!(
328 transport.tags,
329 vec![vec!["n".to_string(), "17".to_string()]]
330 );
331
332 let result = crate::nuts::nut18::transport::TransportBuilder::default().build();
334 assert!(result.is_err());
335 }
336
337 #[test]
338 fn test_nut10_secret_request() {
339 use crate::nuts::nut10::Kind;
340
341 let secret_request = Nut10SecretRequest::new(
343 Kind::P2PK,
344 "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
345 Some(vec![vec!["key".to_string(), "value".to_string()]]),
346 );
347
348 let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
350
351 assert_eq!(full_secret.kind(), Kind::P2PK);
353 assert_eq!(
354 full_secret.secret_data().data(),
355 "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
356 );
357 assert_eq!(
358 full_secret.secret_data().tags().clone(),
359 Some(vec![vec!["key".to_string(), "value".to_string()]]).as_ref()
360 );
361
362 let converted_back = Nut10SecretRequest::from(full_secret);
364
365 assert_eq!(converted_back.kind, secret_request.kind);
367 assert_eq!(converted_back.data, secret_request.data);
368 assert_eq!(converted_back.tags, secret_request.tags);
369
370 let payment_request = PaymentRequest::builder()
372 .payment_id("test123")
373 .amount(Amount::from(100))
374 .nut10(secret_request.clone())
375 .build();
376
377 assert_eq!(payment_request.nut10, Some(secret_request));
378 }
379
380 #[test]
381 fn test_nut10_secret_request_multiple_mints() {
382 let mint_urls = [
383 "https://8333.space:3338",
384 "https://mint.minibits.cash/Bitcoin",
385 "https://antifiat.cash",
386 "https://mint.macadamia.cash",
387 ]
388 .iter()
389 .map(|m| MintUrl::from_str(m).unwrap())
390 .collect();
391
392 let payment_request = PaymentRequestBuilder::default()
393 .unit(CurrencyUnit::Sat)
394 .amount(10)
395 .mints(mint_urls)
396 .build();
397
398 let payment_request_str = payment_request.to_string();
399
400 let r = PaymentRequest::from_str(&payment_request_str).unwrap();
401
402 assert_eq!(payment_request, r);
403 }
404
405 #[test]
406 fn test_nut10_secret_request_htlc() {
407 let bolt11 = "lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9";
408
409 let bolt11 = Bolt11Invoice::from_str(bolt11).unwrap();
410
411 let nut10 = SpendingConditions::HTLCConditions {
412 data: *bolt11.payment_hash(),
413 conditions: None,
414 };
415
416 let payment_request = PaymentRequestBuilder::default()
417 .unit(CurrencyUnit::Sat)
418 .amount(10)
419 .nut10(nut10.into())
420 .build();
421
422 let payment_request_str = payment_request.to_string();
423
424 let r = PaymentRequest::from_str(&payment_request_str).unwrap();
425
426 assert_eq!(payment_request, r);
427 }
428
429 #[test]
430 fn test_nut10_secret_request_p2pk() {
431 let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
433
434 let nut10 = SpendingConditions::P2PKConditions {
436 data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
437 conditions: None,
438 };
439
440 let payment_request = PaymentRequestBuilder::default()
442 .unit(CurrencyUnit::Sat)
443 .amount(10)
444 .payment_id("test-p2pk-id")
445 .description("P2PK locked payment")
446 .nut10(nut10.into())
447 .build();
448
449 let payment_request_str = payment_request.to_string();
451
452 let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
454
455 assert_eq!(payment_request, decoded_request);
457
458 if let Some(nut10_secret) = decoded_request.nut10 {
460 assert_eq!(nut10_secret.kind, Kind::P2PK);
461 assert_eq!(nut10_secret.data, pubkey_hex);
462 } else {
463 panic!("NUT10 secret data missing in decoded payment request");
464 }
465 }
466
467 #[test]
471 fn test_basic_payment_request() {
472 let json = r#"{
474 "i": "b7a90176",
475 "a": 10,
476 "u": "sat",
477 "m": ["https://8333.space:3338"],
478 "t": [
479 {
480 "t": "nostr",
481 "a": "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n",
482 "g": [["n", "17"]]
483 }
484 ]
485 }"#;
486
487 let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=";
488
489 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
491 let payment_request_cloned = payment_request.clone();
492
493 assert_eq!(
495 payment_request_cloned.payment_id.as_ref().unwrap(),
496 "b7a90176"
497 );
498 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
499 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
500 assert_eq!(
501 payment_request_cloned.mints,
502 vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
503 );
504
505 let transport = payment_request.transports.first().unwrap();
506 assert_eq!(transport._type, TransportType::Nostr);
507 assert_eq!(transport.target, "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n");
508 assert_eq!(
509 transport.tags,
510 vec![vec!["n".to_string(), "17".to_string()]]
511 );
512
513 let encoded = payment_request.to_string();
515
516 let decoded = PaymentRequest::from_str(&encoded).unwrap();
518 assert_eq!(payment_request, decoded);
519
520 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
522 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
523 assert_eq!(decoded_from_spec.amount.unwrap(), Amount::from(10));
524 assert_eq!(decoded_from_spec.unit.unwrap(), CurrencyUnit::Sat);
525 assert_eq!(
526 decoded_from_spec.mints,
527 vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
528 );
529 }
530
531 #[test]
532 fn test_nostr_transport_payment_request() {
533 let json = r#"{
535 "i": "f92a51b8",
536 "a": 100,
537 "u": "sat",
538 "m": ["https://mint1.example.com", "https://mint2.example.com"],
539 "t": [
540 {
541 "t": "nostr",
542 "a": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3",
543 "g": [["n", "17"], ["n", "9735"]]
544 }
545 ]
546 }"#;
547
548 let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheD9ucHViMXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXEyOHNwajNhZ4KCYW5iMTeCYW5kOTczNWFpaGY5MmE1MWI4YWEYZGF1Y3NhdGFtgngZaHR0cHM6Ly9taW50MS5leGFtcGxlLmNvbXgZaHR0cHM6Ly9taW50Mi5leGFtcGxlLmNvbQ==";
549
550 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
552 let payment_request_cloned = payment_request.clone();
553
554 assert_eq!(
556 payment_request_cloned.payment_id.as_ref().unwrap(),
557 "f92a51b8"
558 );
559 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
560 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
561 assert_eq!(
562 payment_request_cloned.mints,
563 vec![
564 MintUrl::from_str("https://mint1.example.com").unwrap(),
565 MintUrl::from_str("https://mint2.example.com").unwrap()
566 ]
567 );
568
569 let transport = payment_request_cloned.transports.first().unwrap();
570 assert_eq!(transport._type, TransportType::Nostr);
571 assert_eq!(
572 transport.target,
573 "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3"
574 );
575 assert_eq!(
576 transport.tags,
577 vec![
578 vec!["n".to_string(), "17".to_string()],
579 vec!["n".to_string(), "9735".to_string()]
580 ]
581 );
582
583 let encoded = payment_request.to_string();
585 let decoded = PaymentRequest::from_str(&encoded).unwrap();
586 assert_eq!(payment_request, decoded);
587
588 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
590 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
591 }
592
593 #[test]
594 fn test_minimal_payment_request() {
595 let json = r#"{
597 "i": "7f4a2b39",
598 "u": "sat",
599 "m": ["https://mint.example.com"]
600 }"#;
601
602 let expected_encoded =
603 "creqAo2FpaDdmNGEyYjM5YXVjc2F0YW2BeBhodHRwczovL21pbnQuZXhhbXBsZS5jb20=";
604
605 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
607 let payment_request_cloned = payment_request.clone();
608
609 assert_eq!(
611 payment_request_cloned.payment_id.as_ref().unwrap(),
612 "7f4a2b39"
613 );
614 assert_eq!(payment_request_cloned.amount, None);
615 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
616 assert_eq!(
617 payment_request_cloned.mints,
618 vec![MintUrl::from_str("https://mint.example.com").unwrap()]
619 );
620 assert_eq!(payment_request_cloned.transports, vec![]);
621
622 let encoded = payment_request.to_string();
624 let decoded = PaymentRequest::from_str(&encoded).unwrap();
625 assert_eq!(payment_request, decoded);
626
627 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
629 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
630 }
631
632 #[test]
633 fn test_nut10_locking_payment_request() {
634 let json = r#"{
636 "i": "c9e45d2a",
637 "a": 500,
638 "u": "sat",
639 "m": ["https://mint.example.com"],
640 "nut10": {
641 "k": "P2PK",
642 "d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
643 "t": [["timeout", "3600"]]
644 }
645 }"#;
646
647 let expected_encoded = "creqApWFpaGM5ZTQ1ZDJhYWEZAfRhdWNzYXRhbYF4GGh0dHBzOi8vbWludC5leGFtcGxlLmNvbWVudXQxMKNha2RQMlBLYWR4QjAyYzNiNWJiMjdlMzYxNDU3YzkyZDkzZDc4ZGQ3M2QzZDUzNzMyMTEwYjJjZmU4YjUwZmJjMGFiYzYxNWU5YzMzMWF0gYJndGltZW91dGQzNjAw";
648
649 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
651 let payment_request_cloned = payment_request.clone();
652
653 assert_eq!(
655 payment_request_cloned.payment_id.as_ref().unwrap(),
656 "c9e45d2a"
657 );
658 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
659 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
660 assert_eq!(
661 payment_request_cloned.mints,
662 vec![MintUrl::from_str("https://mint.example.com").unwrap()]
663 );
664
665 let nut10 = payment_request_cloned.nut10.unwrap();
667 assert_eq!(nut10.kind, Kind::P2PK);
668 assert_eq!(
669 nut10.data,
670 "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
671 );
672 assert_eq!(
673 nut10.tags,
674 Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
675 );
676
677 let encoded = payment_request.to_string();
679 let decoded = PaymentRequest::from_str(&encoded).unwrap();
680 assert_eq!(payment_request, decoded);
681
682 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
684 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
685 }
686
687 #[test]
688 fn test_from_str_handles_both_formats() {
689 let payment_request = PaymentRequest {
691 payment_id: Some("test456".to_string()),
692 amount: Some(Amount::from(100)),
693 unit: Some(CurrencyUnit::Sat),
694 single_use: None,
695 mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
696 description: Some("Test both formats".to_string()),
697 transports: vec![],
698 nut10: None,
699 };
700
701 let cbor_encoded = payment_request.to_string();
703 assert!(cbor_encoded.starts_with("creqA"));
704 let decoded_cbor =
705 PaymentRequest::from_str(&cbor_encoded).expect("Should decode CBOR format");
706 assert_eq!(decoded_cbor.payment_id, payment_request.payment_id);
707 assert_eq!(decoded_cbor.amount, payment_request.amount);
708 assert_eq!(decoded_cbor.unit, payment_request.unit);
709 assert_eq!(decoded_cbor.description, payment_request.description);
710
711 let bech32_encoded = payment_request
713 .to_bech32_string()
714 .expect("Should encode to bech32");
715 assert!(bech32_encoded.to_uppercase().starts_with("CREQB"));
716 let decoded_bech32 =
717 PaymentRequest::from_str(&bech32_encoded).expect("Should decode bech32 format");
718 assert_eq!(decoded_bech32.payment_id, payment_request.payment_id);
719 assert_eq!(decoded_bech32.amount, payment_request.amount);
720 assert_eq!(decoded_bech32.unit, payment_request.unit);
721 assert_eq!(decoded_bech32.description, payment_request.description);
722
723 let bech32_lowercase = bech32_encoded.to_lowercase();
725 let decoded_lowercase =
726 PaymentRequest::from_str(&bech32_lowercase).expect("Should decode lowercase bech32");
727 assert_eq!(decoded_lowercase.payment_id, payment_request.payment_id);
728
729 let bech32_uppercase = bech32_encoded.to_uppercase();
730 let decoded_uppercase =
731 PaymentRequest::from_str(&bech32_uppercase).expect("Should decode uppercase bech32");
732 assert_eq!(decoded_uppercase.payment_id, payment_request.payment_id);
733 }
734}