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