cashu/nuts/nut18/
payment_request.rs

1//! NUT-18: Payment Requests
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/18.md>
4
5use 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/// Payment Request
20#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
21pub struct PaymentRequest {
22    /// `Payment id`
23    #[serde(rename = "i")]
24    pub payment_id: Option<String>,
25    /// Amount
26    #[serde(rename = "a")]
27    pub amount: Option<Amount>,
28    /// Unit
29    #[serde(rename = "u")]
30    pub unit: Option<CurrencyUnit>,
31    /// Single use
32    #[serde(rename = "s")]
33    pub single_use: Option<bool>,
34    /// Mints
35    #[serde(rename = "m")]
36    pub mints: Option<Vec<MintUrl>>,
37    /// Description
38    #[serde(rename = "d")]
39    pub description: Option<String>,
40    /// Transport
41    #[serde(rename = "t")]
42    #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")]
43    pub transports: Vec<Transport>,
44    /// Nut10
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub nut10: Option<Nut10SecretRequest>,
47}
48
49impl PaymentRequest {
50    /// Create a new PaymentRequestBuilder
51    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/// Builder for PaymentRequest
89#[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    /// Set payment ID
103    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    /// Set amount
112    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    /// Set unit
121    pub fn unit(mut self, unit: CurrencyUnit) -> Self {
122        self.unit = Some(unit);
123        self
124    }
125
126    /// Set single use flag
127    pub fn single_use(mut self, single_use: bool) -> Self {
128        self.single_use = Some(single_use);
129        self
130    }
131
132    /// Add a mint URL
133    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    /// Set mints
139    pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
140        self.mints = Some(mints);
141        self
142    }
143
144    /// Set description
145    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
146        self.description = Some(description.into());
147        self
148    }
149
150    /// Add a transport
151    pub fn add_transport(mut self, transport: Transport) -> Self {
152        self.transports.push(transport);
153        self
154    }
155
156    /// Set transports
157    pub fn transports(mut self, transports: Vec<Transport>) -> Self {
158        self.transports = transports;
159        self
160    }
161
162    /// Set Nut10 secret
163    pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
164        self.nut10 = Some(nut10);
165        self
166    }
167
168    /// Build the PaymentRequest
169    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/// Payment Request
184#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
185pub struct PaymentRequestPayload {
186    /// Id
187    pub id: Option<String>,
188    /// Memo
189    pub memo: Option<String>,
190    /// Mint
191    pub mint: MintUrl,
192    /// Unit
193    pub unit: CurrencyUnit,
194    /// Proofs
195    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        // Build a payment request using the builder pattern
277        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        // Verify the built request
286        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        // Test serialization and deserialization
295        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        // Build a transport using the builder pattern
306        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        // Verify the built transport
314        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        // Test error case - missing required fields
322        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        // Create a Nut10SecretRequest
331        let secret_request = Nut10SecretRequest::new(
332            Kind::P2PK,
333            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
334            Some(vec![vec!["key".to_string(), "value".to_string()]]),
335        );
336
337        // Convert to a full Nut10Secret
338        let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
339
340        // Check conversion
341        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        // Convert back to Nut10SecretRequest
352        let converted_back = Nut10SecretRequest::from(full_secret);
353
354        // Check round-trip conversion
355        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        // Test in PaymentRequest builder
360        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        // Use a public key for P2PK condition
421        let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
422
423        // Create P2PK spending conditions
424        let nut10 = SpendingConditions::P2PKConditions {
425            data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
426            conditions: None,
427        };
428
429        // Build payment request with P2PK condition
430        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        // Convert to string representation
439        let payment_request_str = payment_request.to_string();
440
441        // Parse back from string
442        let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
443
444        // Verify round-trip serialization
445        assert_eq!(payment_request, decoded_request);
446
447        // Verify the P2PK data was preserved correctly
448        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 vectors from NUT-18 specification
457    /// https://github.com/cashubtc/nuts/blob/main/tests/18-tests.md
458
459    #[test]
460    fn test_basic_payment_request() {
461        // Basic payment request with required fields
462        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        // Parse the JSON into a PaymentRequest
479        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
480        let payment_request_cloned = payment_request.clone();
481
482        // Verify the payment request fields
483        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        // Test encoding - the encoded form should match the expected output
503        let encoded = payment_request.to_string();
504
505        // For now, let's verify it can be decoded back correctly
506        let decoded = PaymentRequest::from_str(&encoded).unwrap();
507        assert_eq!(payment_request, decoded);
508
509        // Test decoding the expected encoded string
510        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        // Nostr transport payment request with multiple mints
523        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        // Parse the JSON into a PaymentRequest
540        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
541        let payment_request_cloned = payment_request.clone();
542
543        // Verify the payment request fields
544        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        // Test round-trip serialization
573        let encoded = payment_request.to_string();
574        let decoded = PaymentRequest::from_str(&encoded).unwrap();
575        assert_eq!(payment_request, decoded);
576
577        // Test decoding the expected encoded string
578        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        // Minimal payment request with only required fields
585        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        // Parse the JSON into a PaymentRequest
595        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
596        let payment_request_cloned = payment_request.clone();
597
598        // Verify the payment request fields
599        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        // Test round-trip serialization
612        let encoded = payment_request.to_string();
613        let decoded = PaymentRequest::from_str(&encoded).unwrap();
614        assert_eq!(payment_request, decoded);
615
616        // Test decoding the expected encoded string
617        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        // Payment request with NUT-10 P2PK locking
624        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        // Parse the JSON into a PaymentRequest
639        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
640        let payment_request_cloned = payment_request.clone();
641
642        // Verify the payment request fields
643        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        // Test NUT-10 locking
655        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        // Test round-trip serialization
667        let encoded = payment_request.to_string();
668        let decoded = PaymentRequest::from_str(&encoded).unwrap();
669        assert_eq!(payment_request, decoded);
670
671        // Test decoding the expected encoded string
672        let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
673        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
674    }
675}