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::nuts::{CurrencyUnit, Proofs};
15use crate::Amount;
16
17const PAYMENT_REQUEST_PREFIX: &str = "creqA";
18
19#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
21pub struct PaymentRequest {
22 #[serde(rename = "i")]
24 pub payment_id: Option<String>,
25 #[serde(rename = "a")]
27 pub amount: Option<Amount>,
28 #[serde(rename = "u")]
30 pub unit: Option<CurrencyUnit>,
31 #[serde(rename = "s")]
33 pub single_use: Option<bool>,
34 #[serde(rename = "m")]
36 pub mints: Option<Vec<MintUrl>>,
37 #[serde(rename = "d")]
39 pub description: Option<String>,
40 #[serde(rename = "t")]
42 #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")]
43 pub transports: Vec<Transport>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub nut10: Option<Nut10SecretRequest>,
47}
48
49impl PaymentRequest {
50 pub fn builder() -> PaymentRequestBuilder {
52 PaymentRequestBuilder::default()
53 }
54}
55
56impl AsRef<Option<String>> for PaymentRequest {
57 fn as_ref(&self) -> &Option<String> {
58 &self.payment_id
59 }
60}
61
62impl fmt::Display for PaymentRequest {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 use serde::ser::Error;
65 let mut data = Vec::new();
66 ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
67 let encoded = general_purpose::URL_SAFE.encode(data);
68 write!(f, "{PAYMENT_REQUEST_PREFIX}{encoded}")
69 }
70}
71
72impl FromStr for PaymentRequest {
73 type Err = Error;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 let s = s
77 .strip_prefix(PAYMENT_REQUEST_PREFIX)
78 .ok_or(Error::InvalidPrefix)?;
79
80 let decode_config = general_purpose::GeneralPurposeConfig::new()
81 .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
82 let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
83
84 Ok(ciborium::from_reader(&decoded[..])?)
85 }
86}
87
88#[derive(Debug, Default, Clone)]
90pub struct PaymentRequestBuilder {
91 payment_id: Option<String>,
92 amount: Option<Amount>,
93 unit: Option<CurrencyUnit>,
94 single_use: Option<bool>,
95 mints: Option<Vec<MintUrl>>,
96 description: Option<String>,
97 transports: Vec<Transport>,
98 nut10: Option<Nut10SecretRequest>,
99}
100
101impl PaymentRequestBuilder {
102 pub fn payment_id<S>(mut self, payment_id: S) -> Self
104 where
105 S: Into<String>,
106 {
107 self.payment_id = Some(payment_id.into());
108 self
109 }
110
111 pub fn amount<A>(mut self, amount: A) -> Self
113 where
114 A: Into<Amount>,
115 {
116 self.amount = Some(amount.into());
117 self
118 }
119
120 pub fn unit(mut self, unit: CurrencyUnit) -> Self {
122 self.unit = Some(unit);
123 self
124 }
125
126 pub fn single_use(mut self, single_use: bool) -> Self {
128 self.single_use = Some(single_use);
129 self
130 }
131
132 pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
134 self.mints.get_or_insert_with(Vec::new).push(mint_url);
135 self
136 }
137
138 pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
140 self.mints = Some(mints);
141 self
142 }
143
144 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
146 self.description = Some(description.into());
147 self
148 }
149
150 pub fn add_transport(mut self, transport: Transport) -> Self {
152 self.transports.push(transport);
153 self
154 }
155
156 pub fn transports(mut self, transports: Vec<Transport>) -> Self {
158 self.transports = transports;
159 self
160 }
161
162 pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
164 self.nut10 = Some(nut10);
165 self
166 }
167
168 pub fn build(self) -> PaymentRequest {
170 PaymentRequest {
171 payment_id: self.payment_id,
172 amount: self.amount,
173 unit: self.unit,
174 single_use: self.single_use,
175 mints: self.mints,
176 description: self.description,
177 transports: self.transports,
178 nut10: self.nut10,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
185pub struct PaymentRequestPayload {
186 pub id: Option<String>,
188 pub memo: Option<String>,
190 pub mint: MintUrl,
192 pub unit: CurrencyUnit,
194 pub proofs: Proofs,
196}
197
198#[cfg(test)]
199mod tests {
200 use std::str::FromStr;
201
202 use lightning_invoice::Bolt11Invoice;
203
204 use super::*;
205 use crate::nuts::nut10::Kind;
206 use crate::nuts::SpendingConditions;
207 use crate::TransportType;
208
209 const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
210
211 #[test]
212 fn test_decode_payment_req() {
213 let req = PaymentRequest::from_str(PAYMENT_REQUEST).expect("valid payment request");
214
215 assert_eq!(&req.payment_id.unwrap(), "b7a90176");
216 assert_eq!(req.amount.unwrap(), 10.into());
217 assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
218 assert_eq!(
219 req.mints.unwrap(),
220 vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
221 );
222 assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
223
224 let transport = req.transports.first().unwrap();
225
226 let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
227
228 assert_eq!(transport, &expected_transport);
229 }
230
231 #[test]
232 fn test_roundtrip_payment_req() {
233 let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
234
235 let request = PaymentRequest {
236 payment_id: Some("b7a90176".to_string()),
237 amount: Some(10.into()),
238 unit: Some(CurrencyUnit::Sat),
239 single_use: None,
240 mints: Some(vec!["https://nofees.testnut.cashu.space"
241 .parse()
242 .expect("valid mint url")]),
243 description: None,
244 transports: vec![transport.clone()],
245 nut10: None,
246 };
247
248 let request_str = request.to_string();
249
250 let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
251
252 assert_eq!(&req.payment_id.unwrap(), "b7a90176");
253 assert_eq!(req.amount.unwrap(), 10.into());
254 assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
255 assert_eq!(
256 req.mints.unwrap(),
257 vec![MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url")]
258 );
259 assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
260
261 let t = req.transports.first().unwrap();
262 assert_eq!(&transport, t);
263 }
264
265 #[test]
266 fn test_payment_request_builder() {
267 let transport = Transport {
268 _type: TransportType::Nostr,
269 target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(),
270 tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
271 };
272
273 let mint_url =
274 MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
275
276 let request = PaymentRequest::builder()
278 .payment_id("b7a90176")
279 .amount(Amount::from(10))
280 .unit(CurrencyUnit::Sat)
281 .add_mint(mint_url.clone())
282 .add_transport(transport.clone())
283 .build();
284
285 assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
287 assert_eq!(request.amount.unwrap(), 10.into());
288 assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
289 assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
290
291 let t = request.transports.first().unwrap();
292 assert_eq!(&transport, t);
293
294 let request_str = request.to_string();
296 let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
297
298 assert_eq!(req.payment_id, request.payment_id);
299 assert_eq!(req.amount, request.amount);
300 assert_eq!(req.unit, request.unit);
301 }
302
303 #[test]
304 fn test_transport_builder() {
305 let transport = Transport::builder()
307 .transport_type(TransportType::Nostr)
308 .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
309 .add_tag(vec!["n".to_string(), "17".to_string()])
310 .build()
311 .expect("Valid transport");
312
313 assert_eq!(transport._type, TransportType::Nostr);
315 assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
316 assert_eq!(
317 transport.tags,
318 Some(vec![vec!["n".to_string(), "17".to_string()]])
319 );
320
321 let result = crate::nuts::nut18::transport::TransportBuilder::default().build();
323 assert!(result.is_err());
324 }
325
326 #[test]
327 fn test_nut10_secret_request() {
328 use crate::nuts::nut10::Kind;
329
330 let secret_request = Nut10SecretRequest::new(
332 Kind::P2PK,
333 "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
334 Some(vec![vec!["key".to_string(), "value".to_string()]]),
335 );
336
337 let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
339
340 assert_eq!(full_secret.kind(), Kind::P2PK);
342 assert_eq!(
343 full_secret.secret_data().data(),
344 "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
345 );
346 assert_eq!(
347 full_secret.secret_data().tags().clone(),
348 Some(vec![vec!["key".to_string(), "value".to_string()]]).as_ref()
349 );
350
351 let converted_back = Nut10SecretRequest::from(full_secret);
353
354 assert_eq!(converted_back.kind, secret_request.kind);
356 assert_eq!(converted_back.data, secret_request.data);
357 assert_eq!(converted_back.tags, secret_request.tags);
358
359 let payment_request = PaymentRequest::builder()
361 .payment_id("test123")
362 .amount(Amount::from(100))
363 .nut10(secret_request.clone())
364 .build();
365
366 assert_eq!(payment_request.nut10, Some(secret_request));
367 }
368
369 #[test]
370 fn test_nut10_secret_request_multiple_mints() {
371 let mint_urls = [
372 "https://8333.space:3338",
373 "https://mint.minibits.cash/Bitcoin",
374 "https://antifiat.cash",
375 "https://mint.macadamia.cash",
376 ]
377 .iter()
378 .map(|m| MintUrl::from_str(m).unwrap())
379 .collect();
380
381 let payment_request = PaymentRequestBuilder::default()
382 .unit(CurrencyUnit::Sat)
383 .amount(10)
384 .mints(mint_urls)
385 .build();
386
387 let payment_request_str = payment_request.to_string();
388
389 let r = PaymentRequest::from_str(&payment_request_str).unwrap();
390
391 assert_eq!(payment_request, r);
392 }
393
394 #[test]
395 fn test_nut10_secret_request_htlc() {
396 let bolt11 = "lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9";
397
398 let bolt11 = Bolt11Invoice::from_str(bolt11).unwrap();
399
400 let nut10 = SpendingConditions::HTLCConditions {
401 data: *bolt11.payment_hash(),
402 conditions: None,
403 };
404
405 let payment_request = PaymentRequestBuilder::default()
406 .unit(CurrencyUnit::Sat)
407 .amount(10)
408 .nut10(nut10.into())
409 .build();
410
411 let payment_request_str = payment_request.to_string();
412
413 let r = PaymentRequest::from_str(&payment_request_str).unwrap();
414
415 assert_eq!(payment_request, r);
416 }
417
418 #[test]
419 fn test_nut10_secret_request_p2pk() {
420 let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
422
423 let nut10 = SpendingConditions::P2PKConditions {
425 data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
426 conditions: None,
427 };
428
429 let payment_request = PaymentRequestBuilder::default()
431 .unit(CurrencyUnit::Sat)
432 .amount(10)
433 .payment_id("test-p2pk-id")
434 .description("P2PK locked payment")
435 .nut10(nut10.into())
436 .build();
437
438 let payment_request_str = payment_request.to_string();
440
441 let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
443
444 assert_eq!(payment_request, decoded_request);
446
447 if let Some(nut10_secret) = decoded_request.nut10 {
449 assert_eq!(nut10_secret.kind, Kind::P2PK);
450 assert_eq!(nut10_secret.data, pubkey_hex);
451 } else {
452 panic!("NUT10 secret data missing in decoded payment request");
453 }
454 }
455
456 #[test]
460 fn test_basic_payment_request() {
461 let json = r#"{
463 "i": "b7a90176",
464 "a": 10,
465 "u": "sat",
466 "m": ["https://8333.space:3338"],
467 "t": [
468 {
469 "t": "nostr",
470 "a": "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
471 "g": [["n", "17"]]
472 }
473 ]
474 }"#;
475
476 let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=";
477
478 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
480 let payment_request_cloned = payment_request.clone();
481
482 assert_eq!(
484 payment_request_cloned.payment_id.as_ref().unwrap(),
485 "b7a90176"
486 );
487 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
488 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
489 assert_eq!(
490 payment_request_cloned.mints.unwrap(),
491 vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
492 );
493
494 let transport = payment_request.transports.first().unwrap();
495 assert_eq!(transport._type, TransportType::Nostr);
496 assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
497 assert_eq!(
498 transport.tags,
499 Some(vec![vec!["n".to_string(), "17".to_string()]])
500 );
501
502 let encoded = payment_request.to_string();
504
505 let decoded = PaymentRequest::from_str(&encoded).unwrap();
507 assert_eq!(payment_request, decoded);
508
509 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
511 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
512 assert_eq!(decoded_from_spec.amount.unwrap(), Amount::from(10));
513 assert_eq!(decoded_from_spec.unit.unwrap(), CurrencyUnit::Sat);
514 assert_eq!(
515 decoded_from_spec.mints.unwrap(),
516 vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
517 );
518 }
519
520 #[test]
521 fn test_nostr_transport_payment_request() {
522 let json = r#"{
524 "i": "f92a51b8",
525 "a": 100,
526 "u": "sat",
527 "m": ["https://mint1.example.com", "https://mint2.example.com"],
528 "t": [
529 {
530 "t": "nostr",
531 "a": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3",
532 "g": [["n", "17"], ["n", "9735"]]
533 }
534 ]
535 }"#;
536
537 let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheD9ucHViMXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXEyOHNwajNhZ4KCYW5iMTeCYW5kOTczNWFpaGY5MmE1MWI4YWEYZGF1Y3NhdGFtgngZaHR0cHM6Ly9taW50MS5leGFtcGxlLmNvbXgZaHR0cHM6Ly9taW50Mi5leGFtcGxlLmNvbQ==";
538
539 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
541 let payment_request_cloned = payment_request.clone();
542
543 assert_eq!(
545 payment_request_cloned.payment_id.as_ref().unwrap(),
546 "f92a51b8"
547 );
548 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
549 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
550 assert_eq!(
551 payment_request_cloned.mints.unwrap(),
552 vec![
553 MintUrl::from_str("https://mint1.example.com").unwrap(),
554 MintUrl::from_str("https://mint2.example.com").unwrap()
555 ]
556 );
557
558 let transport = payment_request_cloned.transports.first().unwrap();
559 assert_eq!(transport._type, TransportType::Nostr);
560 assert_eq!(
561 transport.target,
562 "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3"
563 );
564 assert_eq!(
565 transport.tags,
566 Some(vec![
567 vec!["n".to_string(), "17".to_string()],
568 vec!["n".to_string(), "9735".to_string()]
569 ])
570 );
571
572 let encoded = payment_request.to_string();
574 let decoded = PaymentRequest::from_str(&encoded).unwrap();
575 assert_eq!(payment_request, decoded);
576
577 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
579 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
580 }
581
582 #[test]
583 fn test_minimal_payment_request() {
584 let json = r#"{
586 "i": "7f4a2b39",
587 "u": "sat",
588 "m": ["https://mint.example.com"]
589 }"#;
590
591 let expected_encoded =
592 "creqAo2FpaDdmNGEyYjM5YXVjc2F0YW2BeBhodHRwczovL21pbnQuZXhhbXBsZS5jb20=";
593
594 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
596 let payment_request_cloned = payment_request.clone();
597
598 assert_eq!(
600 payment_request_cloned.payment_id.as_ref().unwrap(),
601 "7f4a2b39"
602 );
603 assert_eq!(payment_request_cloned.amount, None);
604 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
605 assert_eq!(
606 payment_request_cloned.mints.unwrap(),
607 vec![MintUrl::from_str("https://mint.example.com").unwrap()]
608 );
609 assert_eq!(payment_request_cloned.transports, vec![]);
610
611 let encoded = payment_request.to_string();
613 let decoded = PaymentRequest::from_str(&encoded).unwrap();
614 assert_eq!(payment_request, decoded);
615
616 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
618 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
619 }
620
621 #[test]
622 fn test_nut10_locking_payment_request() {
623 let json = r#"{
625 "i": "c9e45d2a",
626 "a": 500,
627 "u": "sat",
628 "m": ["https://mint.example.com"],
629 "nut10": {
630 "k": "P2PK",
631 "d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
632 "t": [["timeout", "3600"]]
633 }
634 }"#;
635
636 let expected_encoded = "creqApWFpaGM5ZTQ1ZDJhYWEZAfRhdWNzYXRhbYF4GGh0dHBzOi8vbWludC5leGFtcGxlLmNvbWVudXQxMKNha2RQMlBLYWR4QjAyYzNiNWJiMjdlMzYxNDU3YzkyZDkzZDc4ZGQ3M2QzZDUzNzMyMTEwYjJjZmU4YjUwZmJjMGFiYzYxNWU5YzMzMWF0gYJndGltZW91dGQzNjAw";
637
638 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
640 let payment_request_cloned = payment_request.clone();
641
642 assert_eq!(
644 payment_request_cloned.payment_id.as_ref().unwrap(),
645 "c9e45d2a"
646 );
647 assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
648 assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
649 assert_eq!(
650 payment_request_cloned.mints.unwrap(),
651 vec![MintUrl::from_str("https://mint.example.com").unwrap()]
652 );
653
654 let nut10 = payment_request_cloned.nut10.unwrap();
656 assert_eq!(nut10.kind, Kind::P2PK);
657 assert_eq!(
658 nut10.data,
659 "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
660 );
661 assert_eq!(
662 nut10.tags,
663 Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
664 );
665
666 let encoded = payment_request.to_string();
668 let decoded = PaymentRequest::from_str(&encoded).unwrap();
669 assert_eq!(payment_request, decoded);
670
671 let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
673 assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
674 }
675}