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::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/// Payment Request
21#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PaymentRequest {
23    /// `Payment id`
24    #[serde(rename = "i")]
25    pub payment_id: Option<String>,
26    /// Amount
27    #[serde(rename = "a")]
28    pub amount: Option<Amount>,
29    /// Unit
30    #[serde(rename = "u")]
31    pub unit: Option<CurrencyUnit>,
32    /// Single use
33    #[serde(rename = "s")]
34    pub single_use: Option<bool>,
35    /// Mints
36    #[serde(rename = "m")]
37    pub mints: Option<Vec<MintUrl>>,
38    /// Description
39    #[serde(rename = "d")]
40    pub description: Option<String>,
41    /// Transport
42    #[serde(rename = "t")]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub transports: Option<Vec<Transport>>,
45    /// Nut10
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub nut10: Option<Nut10SecretRequest>,
48}
49
50impl PaymentRequest {
51    /// Create a new PaymentRequestBuilder
52    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/// Builder for PaymentRequest
90#[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    /// Set payment ID
104    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    /// Set amount
113    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    /// Set unit
122    pub fn unit(mut self, unit: CurrencyUnit) -> Self {
123        self.unit = Some(unit);
124        self
125    }
126
127    /// Set single use flag
128    pub fn single_use(mut self, single_use: bool) -> Self {
129        self.single_use = Some(single_use);
130        self
131    }
132
133    /// Add a mint URL
134    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    /// Set mints
140    pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
141        self.mints = Some(mints);
142        self
143    }
144
145    /// Set description
146    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
147        self.description = Some(description.into());
148        self
149    }
150
151    /// Add a transport
152    pub fn add_transport(mut self, transport: Transport) -> Self {
153        self.transports.push(transport);
154        self
155    }
156
157    /// Set transports
158    pub fn transports(mut self, transports: Vec<Transport>) -> Self {
159        self.transports = transports;
160        self
161    }
162
163    /// Set Nut10 secret
164    pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
165        self.nut10 = Some(nut10);
166        self
167    }
168
169    /// Build the PaymentRequest
170    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/// Payment Request
187#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
188pub struct PaymentRequestPayload {
189    /// Id
190    pub id: Option<String>,
191    /// Memo
192    pub memo: Option<String>,
193    /// Mint
194    pub mint: MintUrl,
195    /// Unit
196    pub unit: CurrencyUnit,
197    /// Proofs
198    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        // Build a payment request using the builder pattern
282        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        // Verify the built request
291        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        // Test serialization and deserialization
301        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        // Build a transport using the builder pattern
312        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        // Verify the built transport
320        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        // Test error case - missing required fields
328        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        // Create a Nut10SecretRequest
337        let secret_request = Nut10SecretRequest::new(
338            Kind::P2PK,
339            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
340            Some(vec![vec!["key".to_string(), "value".to_string()]]),
341        );
342
343        // Convert to a full Nut10Secret
344        let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
345
346        // Check conversion
347        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        // Convert back to Nut10SecretRequest
358        let converted_back = Nut10SecretRequest::from(full_secret);
359
360        // Check round-trip conversion
361        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        // Test in PaymentRequest builder
366        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        // Use a public key for P2PK condition
427        let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
428
429        // Create P2PK spending conditions
430        let nut10 = SpendingConditions::P2PKConditions {
431            data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
432            conditions: None,
433        };
434
435        // Build payment request with P2PK condition
436        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        // Convert to string representation
445        let payment_request_str = payment_request.to_string();
446
447        // Parse back from string
448        let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
449
450        // Verify round-trip serialization
451        assert_eq!(payment_request, decoded_request);
452
453        // Verify the P2PK data was preserved correctly
454        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 vectors from NUT-18 specification
463    /// https://github.com/cashubtc/nuts/blob/main/tests/18-tests.md
464
465    #[test]
466    fn test_basic_payment_request() {
467        // Basic payment request with required fields
468        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        // Parse the JSON into a PaymentRequest
485        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
486        let payment_request_cloned = payment_request.clone();
487
488        // Verify the payment request fields
489        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        // Test encoding - the encoded form should match the expected output
510        let encoded = payment_request.to_string();
511
512        // For now, let's verify it can be decoded back correctly
513        let decoded = PaymentRequest::from_str(&encoded).unwrap();
514        assert_eq!(payment_request, decoded);
515
516        // Test decoding the expected encoded string
517        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        // Nostr transport payment request with multiple mints
530        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        // Parse the JSON into a PaymentRequest
547        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
548        let payment_request_cloned = payment_request.clone();
549
550        // Verify the payment request fields
551        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        // Test round-trip serialization
581        let encoded = payment_request.to_string();
582        let decoded = PaymentRequest::from_str(&encoded).unwrap();
583        assert_eq!(payment_request, decoded);
584
585        // Test decoding the expected encoded string
586        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        // Minimal payment request with only required fields
593        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        // Parse the JSON into a PaymentRequest
603        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
604        let payment_request_cloned = payment_request.clone();
605
606        // Verify the payment request fields
607        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        // Test round-trip serialization
620        let encoded = payment_request.to_string();
621        let decoded = PaymentRequest::from_str(&encoded).unwrap();
622        assert_eq!(payment_request, decoded);
623
624        // Test decoding the expected encoded string
625        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        // Payment request with NUT-10 P2PK locking
632        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        // Parse the JSON into a PaymentRequest
647        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
648        let payment_request_cloned = payment_request.clone();
649
650        // Verify the payment request fields
651        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        // Test NUT-10 locking
663        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        // Test round-trip serialization
675        let encoded = payment_request.to_string();
676        let decoded = PaymentRequest::from_str(&encoded).unwrap();
677        assert_eq!(payment_request, decoded);
678
679        // Test decoding the expected encoded string
680        let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
681        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
682    }
683}