Skip to main content

cashu/nuts/nut26/
encoding.rs

1//! NUT-26: Bech32m encoding for payment requests
2//!
3//! This module provides bech32m encoding and decoding functionality for Cashu payment requests,
4//! implementing the CREQ-B format using TLV (Tag-Length-Value) encoding as specified in NUT-26.
5
6use std::str::FromStr;
7
8use bitcoin::bech32::{self, Bech32, Bech32m, Hrp};
9
10use super::Error;
11use crate::mint_url::MintUrl;
12use crate::nuts::nut10::Kind;
13use crate::nuts::nut18::{Nut10SecretRequest, PaymentRequest, Transport, TransportType};
14use crate::nuts::CurrencyUnit;
15use crate::Amount;
16
17/// Human-readable part for CREQ-B bech32m encoding
18pub const CREQ_B_HRP: &str = "creqb";
19
20/// Unit representation for TLV encoding
21#[derive(Debug, Clone, PartialEq, Eq)]
22enum TlvUnit {
23    Sat,
24    Custom(String),
25}
26
27impl From<CurrencyUnit> for TlvUnit {
28    fn from(unit: CurrencyUnit) -> Self {
29        match unit {
30            CurrencyUnit::Sat => TlvUnit::Sat,
31            CurrencyUnit::Msat => TlvUnit::Custom("msat".to_string()),
32            CurrencyUnit::Usd => TlvUnit::Custom("usd".to_string()),
33            CurrencyUnit::Eur => TlvUnit::Custom("eur".to_string()),
34            CurrencyUnit::Custom(c) => TlvUnit::Custom(c),
35            CurrencyUnit::Auth => TlvUnit::Custom("auth".to_string()),
36        }
37    }
38}
39
40impl From<TlvUnit> for CurrencyUnit {
41    fn from(unit: TlvUnit) -> Self {
42        match unit {
43            TlvUnit::Sat => CurrencyUnit::Sat,
44            TlvUnit::Custom(s) => match s.as_str() {
45                "msat" => CurrencyUnit::Msat,
46                "usd" => CurrencyUnit::Usd,
47                "eur" => CurrencyUnit::Eur,
48                "auth" => CurrencyUnit::Auth,
49                _ => CurrencyUnit::Custom(s), // preserve unknown units
50            },
51        }
52    }
53}
54
55/// TLV reader helper for parsing binary TLV data
56struct TlvReader<'a> {
57    data: &'a [u8],
58    position: usize,
59}
60
61impl<'a> TlvReader<'a> {
62    fn new(data: &'a [u8]) -> Self {
63        Self { data, position: 0 }
64    }
65
66    fn read_tlv(&mut self) -> Result<Option<(u8, Vec<u8>)>, Error> {
67        if self.position + 3 > self.data.len() {
68            return Ok(None);
69        }
70
71        let tag = self.data[self.position];
72        let len = u16::from_be_bytes([self.data[self.position + 1], self.data[self.position + 2]])
73            as usize;
74        self.position += 3;
75
76        if self.position + len > self.data.len() {
77            return Err(Error::InvalidLength);
78        }
79
80        let value = self.data[self.position..self.position + len].to_vec();
81        self.position += len;
82
83        Ok(Some((tag, value)))
84    }
85}
86
87/// TLV writer helper for creating binary TLV data
88struct TlvWriter {
89    data: Vec<u8>,
90}
91
92impl TlvWriter {
93    fn new() -> Self {
94        Self { data: Vec::new() }
95    }
96
97    fn write_tlv(&mut self, tag: u8, value: &[u8]) -> Result<(), Error> {
98        let len = u16::try_from(value.len()).map_err(|_| Error::InvalidLength)?;
99        self.data.push(tag);
100        self.data.extend_from_slice(&len.to_be_bytes());
101        self.data.extend_from_slice(value);
102        Ok(())
103    }
104
105    fn into_bytes(self) -> Vec<u8> {
106        self.data
107    }
108}
109
110/// CREQ-B encoding and decoding implementation
111impl PaymentRequest {
112    /// Encodes a payment request to CREQB1 bech32m format.
113    ///
114    /// This function serializes a payment request according to the NUT-26 specification
115    /// and encodes it using the bech32m encoding scheme with the "creqb" human-readable
116    /// part (HRP). The output is always uppercase for optimal QR code compatibility.
117    ///
118    /// # Returns
119    ///
120    /// Returns a `Result` containing:
121    /// * `Ok(String)` - The bech32m-encoded payment request string in uppercase
122    /// * `Err(Error)` - If serialization or encoding fails
123    ///
124    /// # Errors
125    ///
126    /// This function will return an error if:
127    /// * The payment request cannot be serialized to TLV format
128    /// * The bech32m encoding process fails
129    ///
130    /// # Specification
131    ///
132    /// See [NUT-26](https://github.com/cashubtc/nuts/blob/main/26.md) for the complete
133    /// specification of the CREQB1 payment request format.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use std::str::FromStr;
139    ///
140    /// use cashu::nuts::nut18::PaymentRequest;
141    /// use cashu::{Amount, MintUrl};
142    ///
143    /// let payment_request = PaymentRequest {
144    ///     payment_id: Some("test123".to_string()),
145    ///     amount: Some(Amount::from(1000)),
146    ///     unit: Some(cashu::nuts::CurrencyUnit::Sat),
147    ///     single_use: None,
148    ///     mints: vec![MintUrl::from_str("https://mint.example.com")?],
149    ///     description: None,
150    ///     transports: vec![],
151    ///     nut10: None,
152    /// };
153    ///
154    /// let encoded = payment_request.to_bech32_string()?;
155    /// assert!(encoded.starts_with("CREQB1"));
156    /// # Ok::<(), Box<dyn std::error::Error>>(())
157    /// ```
158    pub fn to_bech32_string(&self) -> Result<String, Error> {
159        let tlv_bytes = self.encode_tlv()?;
160        let hrp = Hrp::parse(CREQ_B_HRP).map_err(|_| Error::InvalidStructure)?;
161
162        // Always emit uppercase for QR compatibility
163        let encoded = bech32::encode_upper::<Bech32m>(hrp, &tlv_bytes)
164            .map_err(|_| Error::InvalidStructure)?;
165        Ok(encoded)
166    }
167
168    /// Decodes a payment request from CREQB1 bech32m format.
169    ///
170    /// This function takes a bech32m-encoded payment request string (case-insensitive)
171    /// with the "creqb" human-readable part and deserializes it back into a
172    /// payment request according to the NUT-26 specification.
173    ///
174    /// # Arguments
175    ///
176    /// * `s` - The bech32m-encoded payment request string (case-insensitive)
177    ///
178    /// # Returns
179    ///
180    /// Returns a `Result` containing:
181    /// * `Ok(PaymentRequest)` - The decoded payment request
182    /// * `Err(Error)` - If decoding or deserialization fails
183    ///
184    /// # Errors
185    ///
186    /// This function will return an error if:
187    /// * The input string is not valid bech32m encoding
188    /// * The human-readable part is not "creqb" (case-insensitive)
189    /// * The decoded data cannot be deserialized into a valid payment request
190    /// * The TLV structure is malformed
191    ///
192    /// # Specification
193    ///
194    /// See [NUT-26](https://github.com/cashubtc/nuts/blob/main/26.md) for the complete
195    /// specification of the CREQB1 payment request format.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use cashu::nuts::nut18::PaymentRequest;
201    ///
202    /// let encoded = "CREQB1QYQQWAR9WD6RZV3NQ5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKS4U8XXF";
203    /// let payment_request = PaymentRequest::from_bech32_string(encoded)?;
204    /// assert_eq!(payment_request.payment_id, Some("test123".to_string()));
205    /// # Ok::<(), cashu::nuts::nut26::Error>(())
206    /// ```
207    pub fn from_bech32_string(s: &str) -> Result<Self, Error> {
208        let (hrp, data) = bech32::decode(s).map_err(Error::Bech32Error)?;
209        if !hrp.as_str().eq_ignore_ascii_case(CREQ_B_HRP) {
210            return Err(Error::InvalidPrefix);
211        }
212
213        Self::from_bech32_bytes(&data)
214    }
215
216    /// Decode from TLV bytes
217    fn from_bech32_bytes(bytes: &[u8]) -> Result<PaymentRequest, Error> {
218        let mut reader = TlvReader::new(bytes);
219
220        let mut id: Option<String> = None;
221        let mut amount: Option<Amount> = None;
222        let mut unit: Option<CurrencyUnit> = None;
223        let mut single_use: Option<bool> = None;
224        let mut mints: Vec<MintUrl> = Vec::new();
225        let mut description: Option<String> = None;
226        let mut transports: Vec<Transport> = Vec::new();
227        let mut nut10: Option<Nut10SecretRequest> = None;
228
229        while let Some((tag, value)) = reader.read_tlv()? {
230            match tag {
231                0x01 => {
232                    // id: string
233                    if id.is_some() {
234                        return Err(Error::InvalidStructure);
235                    }
236                    id = Some(String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?);
237                }
238                0x02 => {
239                    // amount: u64
240                    if amount.is_some() {
241                        return Err(Error::InvalidStructure);
242                    }
243                    if value.len() != 8 {
244                        return Err(Error::InvalidLength);
245                    }
246                    let amount_val = u64::from_be_bytes([
247                        value[0], value[1], value[2], value[3], value[4], value[5], value[6],
248                        value[7],
249                    ]);
250                    amount = Some(Amount::from(amount_val));
251                }
252                0x03 => {
253                    // unit: u8 or string
254                    if unit.is_some() {
255                        return Err(Error::InvalidStructure);
256                    }
257                    if value.len() == 1 && value[0] == 0 {
258                        unit = Some(CurrencyUnit::Sat);
259                    } else {
260                        let unit_str = String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?;
261                        unit = Some(TlvUnit::Custom(unit_str).into());
262                    }
263                }
264                0x04 => {
265                    // single_use: u8 (0 or 1)
266                    if single_use.is_some() {
267                        return Err(Error::InvalidStructure);
268                    }
269                    if !value.is_empty() {
270                        single_use = Some(value[0] != 0);
271                    }
272                }
273                0x05 => {
274                    // mint: string (repeatable)
275                    let mint_str = String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?;
276                    let mint_url =
277                        MintUrl::from_str(&mint_str).map_err(|_| Error::InvalidStructure)?;
278                    mints.push(mint_url);
279                }
280                0x06 => {
281                    // description: string
282                    if description.is_some() {
283                        return Err(Error::InvalidStructure);
284                    }
285                    description = Some(String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?);
286                }
287                0x07 => {
288                    // transport: sub-TLV (repeatable)
289                    let transport = Self::decode_transport(&value)?;
290                    transports.push(transport);
291                }
292                0x08 => {
293                    // nut10: sub-TLV
294                    if nut10.is_some() {
295                        return Err(Error::InvalidStructure);
296                    }
297                    nut10 = Some(Self::decode_nut10(&value)?);
298                }
299                _ => {
300                    // Unknown tags are ignored
301                }
302            }
303        }
304
305        Ok(PaymentRequest {
306            payment_id: id,
307            amount,
308            unit,
309            single_use,
310            mints,
311            description,
312            transports,
313            nut10,
314        })
315    }
316
317    /// Encode to TLV bytes
318    fn encode_tlv(&self) -> Result<Vec<u8>, Error> {
319        let mut writer = TlvWriter::new();
320
321        // 0x01 id: string
322        if let Some(ref id) = self.payment_id {
323            writer.write_tlv(0x01, id.as_bytes())?;
324        }
325
326        // 0x02 amount: u64
327        if let Some(amount) = self.amount {
328            let amount_bytes = (amount.to_u64()).to_be_bytes();
329            writer.write_tlv(0x02, &amount_bytes)?;
330        }
331
332        // 0x03 unit: u8 or string
333        if let Some(ref unit) = self.unit {
334            let tlv_unit = TlvUnit::from(unit.clone());
335            match tlv_unit {
336                TlvUnit::Sat => writer.write_tlv(0x03, &[0])?,
337                TlvUnit::Custom(s) => writer.write_tlv(0x03, s.as_bytes())?,
338            }
339        }
340
341        // 0x04 single_use: u8 (0 or 1)
342        if let Some(single_use) = self.single_use {
343            writer.write_tlv(0x04, &[if single_use { 1 } else { 0 }])?;
344        }
345
346        // 0x05 mint: string (repeatable)
347        for mint in &self.mints {
348            writer.write_tlv(0x05, mint.to_string().as_bytes())?;
349        }
350
351        // 0x06 description: string
352        if let Some(ref description) = self.description {
353            writer.write_tlv(0x06, description.as_bytes())?;
354        }
355
356        // 0x07 transport: sub-TLV (repeatable, order = priority)
357        // Note: In-band transport is represented by absence of transport tag per NUT-26
358        for transport in &self.transports {
359            let transport_bytes = Self::encode_transport(transport)?;
360            writer.write_tlv(0x07, &transport_bytes)?;
361        }
362
363        // 0x08 nut10: sub-TLV
364        if let Some(ref nut10) = self.nut10 {
365            let nut10_bytes = Self::encode_nut10(nut10)?;
366            writer.write_tlv(0x08, &nut10_bytes)?;
367        }
368
369        Ok(writer.into_bytes())
370    }
371
372    /// Decode transport sub-TLV
373    fn decode_transport(bytes: &[u8]) -> Result<Transport, Error> {
374        let mut reader = TlvReader::new(bytes);
375
376        let mut kind: Option<u8> = None;
377        let mut pubkey: Option<Vec<u8>> = None;
378        let mut tags: Vec<(String, Vec<String>)> = Vec::new();
379        let mut http_target: Option<String> = None;
380
381        while let Some((tag, value)) = reader.read_tlv()? {
382            match tag {
383                0x01 => {
384                    // kind: u8
385                    if kind.is_some() {
386                        return Err(Error::InvalidStructure);
387                    }
388                    if value.len() != 1 {
389                        return Err(Error::InvalidLength);
390                    }
391                    kind = Some(value[0]);
392                }
393                0x02 => {
394                    // target: bytes (interpretation depends on kind)
395                    match kind {
396                        Some(0x00) => {
397                            // nostr: 32-byte x-only pubkey
398                            if value.len() != 32 {
399                                return Err(Error::InvalidLength);
400                            }
401                            pubkey = Some(value);
402                        }
403                        Some(0x01) => {
404                            // http_post: UTF-8 URL string
405                            http_target =
406                                Some(String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?);
407                        }
408                        None => {
409                            // kind should always be present if there's a target
410                        }
411                        _ => return Err(Error::InvalidStructure),
412                    }
413                }
414                0x03 => {
415                    // tag_tuple: generic tuple (repeatable)
416                    let tag_tuple = Self::decode_tag_tuple(&value)?;
417                    tags.push(tag_tuple);
418                }
419                _ => {
420                    // Unknown sub-TLV tags are ignored
421                }
422            }
423        }
424
425        // In-band transport is represented by absence of transport tag (0x07)
426        // If we're here, we have a transport tag, so it must be nostr or http_post
427        let transport_type = match kind.ok_or(Error::InvalidStructure)? {
428            0x00 => TransportType::Nostr,
429            0x01 => TransportType::HttpPost,
430            _ => return Err(Error::InvalidStructure),
431        };
432
433        // Extract relays from "r" tag tuples for Nostr transport
434        let relays: Vec<String> = tags
435            .iter()
436            .filter(|(k, _)| k == "r")
437            .flat_map(|(_, v)| v.clone())
438            .collect();
439
440        // Build the target string based on transport type
441        let target = match transport_type {
442            TransportType::Nostr => {
443                // Always use nprofile (with empty relay list if no relays)
444                if let Some(pk) = pubkey {
445                    Self::encode_nprofile(&pk, &relays)?
446                } else {
447                    return Err(Error::InvalidStructure);
448                }
449            }
450            TransportType::HttpPost => http_target.ok_or(Error::InvalidStructure)?,
451        };
452
453        // Keep tags as-is per NUT-26 spec (no "r" to "relay" conversion)
454        // "r" tags are part of the transport encoding and should be preserved
455        let final_tags: Vec<(String, Vec<String>)> = tags;
456
457        Ok(Transport {
458            _type: transport_type,
459            target,
460            tags: final_tags
461                .into_iter()
462                .map(|(k, v)| {
463                    let mut result = vec![k];
464                    result.extend(v);
465                    result
466                })
467                .collect(),
468        })
469    }
470
471    /// Encode transport to sub-TLV
472    fn encode_transport(transport: &Transport) -> Result<Vec<u8>, Error> {
473        let mut writer = TlvWriter::new();
474
475        // 0x01 kind: u8
476        let kind = match transport._type {
477            TransportType::Nostr => 0x00u8,
478            TransportType::HttpPost => 0x01u8,
479        };
480        writer.write_tlv(0x01, &[kind])?;
481
482        // 0x02 target: bytes
483        match transport._type {
484            TransportType::Nostr => {
485                // For nostr, decode nprofile to extract pubkey and relays
486                let (pubkey, relays) = Self::decode_nprofile(&transport.target)?;
487
488                // Write the 32-byte pubkey
489                writer.write_tlv(0x02, &pubkey)?;
490
491                // Collect all relays (from nprofile and from "relay" tags)
492                let mut all_relays = relays;
493
494                // Extract NIPs and other tags from the tags field
495                for tag in &transport.tags {
496                    if tag.is_empty() {
497                        continue;
498                    }
499                    if tag[0] == "n" && tag.len() >= 2 {
500                        // Encode NIPs as tag tuples with key "n"
501                        let tag_bytes = Self::encode_tag_tuple(tag)?;
502                        writer.write_tlv(0x03, &tag_bytes)?;
503                    } else if tag[0] == "relay" && tag.len() >= 2 {
504                        // Collect relays from tags to encode as "r" tag tuples
505                        all_relays.push(tag[1].clone());
506                    } else {
507                        // Other tags as generic tag tuples
508                        let tag_bytes = Self::encode_tag_tuple(tag)?;
509                        writer.write_tlv(0x03, &tag_bytes)?;
510                    }
511                }
512
513                // 0x03 tag_tuple: encode relays as tag tuples with key "r"
514                for relay in all_relays {
515                    let relay_tag = vec!["r".to_string(), relay];
516                    let tag_bytes = Self::encode_tag_tuple(&relay_tag)?;
517                    writer.write_tlv(0x03, &tag_bytes)?;
518                }
519            }
520            TransportType::HttpPost => {
521                writer.write_tlv(0x02, transport.target.as_bytes())?;
522
523                // 0x03 tag_tuple: generic tuple (repeatable)
524                for tag in &transport.tags {
525                    if !tag.is_empty() {
526                        let tag_bytes = Self::encode_tag_tuple(tag)?;
527                        writer.write_tlv(0x03, &tag_bytes)?;
528                    }
529                }
530            }
531        }
532
533        Ok(writer.into_bytes())
534    }
535
536    /// Decode NUT-10 sub-TLV
537    fn decode_nut10(bytes: &[u8]) -> Result<Nut10SecretRequest, Error> {
538        let mut reader = TlvReader::new(bytes);
539
540        let mut kind: Option<u8> = None;
541        let mut data: Option<Vec<u8>> = None;
542        let mut tags: Vec<(String, Vec<String>)> = Vec::new();
543
544        while let Some((tag, value)) = reader.read_tlv()? {
545            match tag {
546                0x01 => {
547                    // kind: u8
548                    if kind.is_some() {
549                        return Err(Error::InvalidStructure);
550                    }
551                    if value.len() != 1 {
552                        return Err(Error::InvalidLength);
553                    }
554                    kind = Some(value[0]);
555                }
556                0x02 => {
557                    // data: bytes
558                    if data.is_some() {
559                        return Err(Error::InvalidStructure);
560                    }
561                    data = Some(value);
562                }
563                0x03 => {
564                    // tag_tuple: generic tuple (repeatable)
565                    let tag_tuple = Self::decode_tag_tuple(&value)?;
566                    tags.push(tag_tuple);
567                }
568                _ => {
569                    // Unknown tags are ignored
570                }
571            }
572        }
573
574        let kind_val = kind.ok_or(Error::InvalidStructure)?;
575        let data_val = data.unwrap_or_default();
576
577        // Convert kind u8 to Kind enum
578        let data_str = String::from_utf8(data_val).map_err(|_| Error::InvalidUtf8)?;
579
580        // Map kind value to Kind enum, error on unknown kinds
581        let kind_enum = match kind_val {
582            0 => Kind::P2PK,
583            1 => Kind::HTLC,
584            _ => return Err(Error::UnknownKind(kind_val)),
585        };
586
587        Ok(Nut10SecretRequest::new(
588            kind_enum,
589            &data_str,
590            if tags.is_empty() {
591                None
592            } else {
593                Some(
594                    tags.into_iter()
595                        .map(|(k, v)| {
596                            let mut result = vec![k];
597                            result.extend(v);
598                            result
599                        })
600                        .collect::<Vec<_>>(),
601                )
602            },
603        ))
604    }
605
606    /// Encode NUT-10 to sub-TLV
607    fn encode_nut10(nut10: &Nut10SecretRequest) -> Result<Vec<u8>, Error> {
608        let mut writer = TlvWriter::new();
609
610        // 0x01 kind: u8
611        let kind_val = match nut10.kind {
612            Kind::P2PK => 0u8,
613            Kind::HTLC => 1u8,
614        };
615        writer.write_tlv(0x01, &[kind_val])?;
616
617        // 0x02 data: bytes
618        writer.write_tlv(0x02, nut10.data.as_bytes())?;
619
620        // 0x03 tag_tuple: generic tuple (repeatable)
621        if let Some(ref tags) = nut10.tags {
622            for tag in tags {
623                let tag_bytes = Self::encode_tag_tuple(tag)?;
624                writer.write_tlv(0x03, &tag_bytes)?;
625            }
626        }
627
628        Ok(writer.into_bytes())
629    }
630
631    /// Decode tag tuple
632    fn decode_tag_tuple(bytes: &[u8]) -> Result<(String, Vec<String>), Error> {
633        if bytes.is_empty() {
634            return Err(Error::InvalidLength);
635        }
636
637        let key_len = bytes[0] as usize;
638        if bytes.len() < 1 + key_len {
639            return Err(Error::InvalidLength);
640        }
641
642        let key =
643            String::from_utf8(bytes[1..1 + key_len].to_vec()).map_err(|_| Error::InvalidUtf8)?;
644
645        let mut values = Vec::new();
646        let mut pos = 1 + key_len;
647
648        while pos < bytes.len() {
649            let val_len = bytes[pos] as usize;
650            pos += 1;
651
652            if pos + val_len > bytes.len() {
653                return Err(Error::InvalidLength);
654            }
655
656            let value = String::from_utf8(bytes[pos..pos + val_len].to_vec())
657                .map_err(|_| Error::InvalidUtf8)?;
658            values.push(value);
659            pos += val_len;
660        }
661
662        Ok((key, values))
663    }
664
665    /// Encode tag tuple
666    fn encode_tag_tuple(tag: &[String]) -> Result<Vec<u8>, Error> {
667        if tag.is_empty() {
668            return Err(Error::InvalidStructure);
669        }
670
671        let mut bytes = Vec::new();
672
673        // Key length + key
674        let key = &tag[0];
675        let key_len = u8::try_from(key.len()).map_err(|_| Error::TagTooLong)?;
676        bytes.push(key_len);
677        bytes.extend_from_slice(key.as_bytes());
678
679        // Values
680        for value in &tag[1..] {
681            let value_len = u8::try_from(value.len()).map_err(|_| Error::TagTooLong)?;
682            bytes.push(value_len);
683            bytes.extend_from_slice(value.as_bytes());
684        }
685
686        Ok(bytes)
687    }
688
689    /// Decode nprofile bech32 string to (pubkey, relays)
690    /// NIP-19 nprofile TLV format:
691    /// - Type 0: 32-byte pubkey (required, only one)
692    /// - Type 1: relay URL string (optional, repeatable)
693    fn decode_nprofile(nprofile: &str) -> Result<(Vec<u8>, Vec<String>), Error> {
694        let (hrp, data) = bech32::decode(nprofile).map_err(Error::Bech32Error)?;
695        if hrp.as_str() != "nprofile" {
696            return Err(Error::InvalidStructure);
697        }
698
699        // Parse NIP-19 TLV format (Type: 1 byte, Length: 1 byte, Value: variable)
700        let mut pos = 0;
701        let mut pubkey: Option<Vec<u8>> = None;
702        let mut relays: Vec<String> = Vec::new();
703
704        while pos < data.len() {
705            if pos + 2 > data.len() {
706                break; // Not enough data for type + length
707            }
708
709            let tag = data[pos];
710            let len = data[pos + 1] as usize;
711            pos += 2;
712
713            if pos + len > data.len() {
714                return Err(Error::InvalidLength);
715            }
716
717            let value = &data[pos..pos + len];
718            pos += len;
719
720            match tag {
721                0 => {
722                    // pubkey: 32 bytes
723                    if value.len() != 32 {
724                        return Err(Error::InvalidLength);
725                    }
726                    pubkey = Some(value.to_vec());
727                }
728                1 => {
729                    // relay: UTF-8 string
730                    let relay =
731                        String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?;
732                    relays.push(relay);
733                }
734                _ => {
735                    // Unknown TLV types are ignored per NIP-19
736                }
737            }
738        }
739
740        let pubkey = pubkey.ok_or(Error::InvalidStructure)?;
741        Ok((pubkey, relays))
742    }
743
744    /// Encode pubkey and relays to nprofile bech32 string
745    /// NIP-19 nprofile TLV format (Type: 1 byte, Length: 1 byte, Value: variable)
746    fn encode_nprofile(pubkey: &[u8], relays: &[String]) -> Result<String, Error> {
747        if pubkey.len() != 32 {
748            return Err(Error::InvalidLength);
749        }
750
751        let mut tlv_bytes = Vec::new();
752
753        // Type 0: pubkey (32 bytes) - Length must fit in 1 byte
754        tlv_bytes.push(0); // type
755        tlv_bytes.push(32); // length
756        tlv_bytes.extend_from_slice(pubkey);
757
758        // Type 1: relays (repeatable) - Length must fit in 1 byte
759        for relay in relays {
760            if relay.len() > 255 {
761                return Err(Error::TagTooLong); // Relay URL too long for NIP-19
762            }
763            tlv_bytes.push(1); // type
764            tlv_bytes.push(relay.len() as u8); // length
765            tlv_bytes.extend_from_slice(relay.as_bytes());
766        }
767
768        let hrp = Hrp::parse("nprofile").map_err(|_| Error::InvalidStructure)?;
769        bech32::encode::<Bech32>(hrp, &tlv_bytes).map_err(|_| Error::InvalidStructure)
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use std::str::FromStr;
776
777    use super::*;
778    use crate::nuts::nut10::Kind;
779    use crate::util::hex;
780    use crate::TransportType;
781
782    #[test]
783    fn test_bech32_basic_round_trip() {
784        let transport = Transport {
785            _type: TransportType::HttpPost,
786            target: "https://api.example.com/payment".to_string(),
787            tags: vec![],
788        };
789
790        let payment_request = PaymentRequest {
791            payment_id: Some("test123".to_string()),
792            amount: Some(Amount::from(100)),
793            unit: Some(CurrencyUnit::Sat),
794            single_use: Some(true),
795            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
796            description: Some("Test payment".to_string()),
797            transports: vec![transport],
798            nut10: None,
799        };
800
801        let encoded = payment_request
802            .to_bech32_string()
803            .expect("encoding should work");
804
805        // Verify it starts with CREQB1
806        assert!(encoded.starts_with("CREQB1"));
807
808        // Round-trip test
809        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
810        assert_eq!(decoded.payment_id, payment_request.payment_id);
811        assert_eq!(decoded.amount, payment_request.amount);
812        assert_eq!(decoded.unit, payment_request.unit);
813        assert_eq!(decoded.single_use, payment_request.single_use);
814        assert_eq!(decoded.description, payment_request.description);
815    }
816
817    #[test]
818    fn test_bech32_minimal() {
819        let payment_request = PaymentRequest {
820            payment_id: Some("minimal".to_string()),
821            amount: None,
822            unit: Some(CurrencyUnit::Sat),
823            single_use: None,
824            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
825            description: None,
826            transports: vec![],
827            nut10: None,
828        };
829
830        let encoded = payment_request
831            .to_bech32_string()
832            .expect("encoding should work");
833
834        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
835        assert_eq!(decoded.payment_id, payment_request.payment_id);
836        assert_eq!(decoded.mints, payment_request.mints);
837    }
838
839    #[test]
840    fn test_bech32_with_nut10() {
841        let nut10 = Nut10SecretRequest::new(
842            Kind::P2PK,
843            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
844            Some(vec![vec!["timeout".to_string(), "3600".to_string()]]),
845        );
846
847        let payment_request = PaymentRequest {
848            payment_id: Some("nut10test".to_string()),
849            amount: Some(Amount::from(500)),
850            unit: Some(CurrencyUnit::Sat),
851            single_use: None,
852            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
853            description: Some("P2PK locked payment".to_string()),
854            transports: vec![],
855            nut10: Some(nut10.clone()),
856        };
857
858        let encoded = payment_request
859            .to_bech32_string()
860            .expect("encoding should work");
861
862        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
863        assert_eq!(decoded.nut10.as_ref().unwrap().kind, nut10.kind);
864        assert_eq!(decoded.nut10.as_ref().unwrap().data, nut10.data);
865    }
866
867    #[test]
868    fn test_parse_creq_param_bech32() {
869        let payment_request = PaymentRequest {
870            payment_id: Some("test123".to_string()),
871            amount: Some(Amount::from(100)),
872            unit: Some(CurrencyUnit::Sat),
873            single_use: None,
874            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
875            description: None,
876            transports: vec![],
877            nut10: None,
878        };
879
880        let encoded = payment_request
881            .to_bech32_string()
882            .expect("encoding should work");
883
884        let decoded_payment_request =
885            PaymentRequest::from_bech32_string(&encoded).expect("should parse bech32");
886        assert_eq!(
887            decoded_payment_request.payment_id,
888            payment_request.payment_id
889        );
890    }
891
892    #[test]
893    fn test_from_bech32_string_errors_on_wrong_encoding() {
894        // Test that from_bech32_string errors if given a non-CREQ-B string
895        let legacy_creq = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
896
897        // Should error because it's not bech32m encoded
898        assert!(PaymentRequest::from_bech32_string(legacy_creq).is_err());
899
900        // Test with a string that's not CREQ-B
901        assert!(PaymentRequest::from_bech32_string("not_a_creq").is_err());
902
903        // Test with wrong HRP (nprofile instead of creqb)
904        let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
905        let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
906        let nprofile =
907            PaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("should encode nprofile");
908        assert!(PaymentRequest::from_bech32_string(&nprofile).is_err());
909    }
910
911    #[test]
912    fn test_unit_encoding_bech32() {
913        // Test default sat unit
914        let payment_request = PaymentRequest {
915            payment_id: Some("unit_test".to_string()),
916            amount: Some(Amount::from(100)),
917            unit: Some(CurrencyUnit::Sat),
918            single_use: None,
919            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
920            description: None,
921            transports: vec![],
922            nut10: None,
923        };
924
925        let encoded = payment_request
926            .to_bech32_string()
927            .expect("encoding should work");
928
929        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
930        assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
931
932        // Test custom unit
933        let payment_request_usd = PaymentRequest {
934            payment_id: Some("unit_test_usd".to_string()),
935            amount: Some(Amount::from(100)),
936            unit: Some(CurrencyUnit::Usd),
937            single_use: None,
938            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
939            description: None,
940            transports: vec![],
941            nut10: None,
942        };
943
944        let encoded_usd = payment_request_usd
945            .to_bech32_string()
946            .expect("encoding should work");
947
948        let decoded_usd =
949            PaymentRequest::from_bech32_string(&encoded_usd).expect("decoding should work");
950        assert_eq!(decoded_usd.unit, Some(CurrencyUnit::Usd));
951    }
952
953    #[test]
954    fn test_nprofile_no_relays() {
955        // Test vector: a known 32-byte pubkey
956        let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
957        let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
958
959        // Encode to nprofile with empty relay list
960        let nprofile =
961            PaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("should encode nprofile");
962        assert!(nprofile.starts_with("nprofile"));
963
964        // Decode back
965        let decoded = PaymentRequest::decode_nprofile(&nprofile).expect("should decode nprofile");
966        assert_eq!(decoded.0, pubkey_bytes);
967        assert!(decoded.1.is_empty());
968    }
969
970    #[test]
971    fn test_nprofile_encoding_decoding() {
972        use nostr_sdk::prelude::*;
973
974        let keys = Keys::generate();
975        let pubkey_bytes = keys.public_key().to_bytes().to_vec();
976        let relays = vec![
977            "wss://relay.example.com".to_string(),
978            "wss://another-relay.example.com".to_string(),
979        ];
980
981        // Encode to nprofile
982        let nprofile = PaymentRequest::encode_nprofile(&pubkey_bytes, &relays)
983            .expect("should encode nprofile");
984        assert!(nprofile.starts_with("nprofile"));
985
986        // Decode back
987        let (decoded_pubkey, decoded_relays) =
988            PaymentRequest::decode_nprofile(&nprofile).expect("should decode nprofile");
989        assert_eq!(decoded_pubkey, pubkey_bytes);
990        assert_eq!(decoded_relays, relays);
991    }
992
993    #[test]
994    fn test_nprofile_matches_nostr_crate() {
995        use nostr_sdk::prelude::*;
996
997        let keys = Keys::generate();
998        let nostr_pubkey = keys.public_key();
999        let pubkey_bytes = nostr_pubkey.to_bytes().to_vec();
1000        let relays = vec![
1001            "wss://relay.example.com".to_string(),
1002            "wss://relay.damus.io".to_string(),
1003        ];
1004
1005        // Create nostr-sdk relay URLs
1006        let nostr_relays: Vec<RelayUrl> = relays
1007            .iter()
1008            .map(|r| RelayUrl::parse(r).expect("valid relay url"))
1009            .collect();
1010
1011        // Test 1: Encode with our implementation, decode with nostr-sdk
1012        let our_nprofile = PaymentRequest::encode_nprofile(&pubkey_bytes, &relays)
1013            .expect("should encode nprofile");
1014
1015        let nostr_decoded =
1016            Nip19Profile::from_bech32(&our_nprofile).expect("nostr-sdk should decode our nprofile");
1017        assert_eq!(nostr_decoded.public_key, nostr_pubkey);
1018        assert_eq!(nostr_decoded.relays.len(), relays.len());
1019        for (decoded_relay, expected_relay) in nostr_decoded.relays.iter().zip(nostr_relays.iter())
1020        {
1021            assert_eq!(decoded_relay, expected_relay);
1022        }
1023
1024        // Test 2: Encode with nostr-sdk, decode with our implementation
1025        let nostr_profile = Nip19Profile::new(nostr_pubkey, nostr_relays.clone());
1026        let nostr_nprofile = nostr_profile.to_bech32().expect("should encode nprofile");
1027
1028        let (our_decoded_pubkey, our_decoded_relays) =
1029            PaymentRequest::decode_nprofile(&nostr_nprofile)
1030                .expect("should decode nostr-sdk nprofile");
1031        assert_eq!(our_decoded_pubkey, pubkey_bytes);
1032        assert_eq!(our_decoded_relays.len(), relays.len());
1033        for (decoded_relay, expected_relay) in our_decoded_relays.iter().zip(relays.iter()) {
1034            assert_eq!(decoded_relay, expected_relay);
1035        }
1036
1037        // Test 3: Both implementations produce identical bech32 strings
1038        assert_eq!(our_nprofile, nostr_nprofile);
1039    }
1040
1041    #[test]
1042    fn test_nprofile_empty_relays_matches_nostr_crate() {
1043        use nostr_sdk::prelude::*;
1044
1045        let keys = Keys::generate();
1046        let nostr_pubkey = keys.public_key();
1047        let pubkey_bytes = nostr_pubkey.to_bytes().to_vec();
1048
1049        // Create nostr-sdk types with empty relays
1050        let nostr_relays: Vec<RelayUrl> = vec![];
1051
1052        // Test with empty relays
1053        let our_nprofile =
1054            PaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("should encode nprofile");
1055
1056        let nostr_profile = Nip19Profile::new(nostr_pubkey, nostr_relays);
1057        let nostr_nprofile = nostr_profile.to_bech32().expect("should encode nprofile");
1058
1059        // Verify both can decode each other's output
1060        let nostr_decoded =
1061            Nip19Profile::from_bech32(&our_nprofile).expect("nostr-sdk should decode our nprofile");
1062        assert_eq!(nostr_decoded.public_key, nostr_pubkey);
1063        assert!(nostr_decoded.relays.is_empty());
1064
1065        let (our_decoded_pubkey, our_decoded_relays) =
1066            PaymentRequest::decode_nprofile(&nostr_nprofile)
1067                .expect("should decode nostr-sdk nprofile");
1068        assert_eq!(our_decoded_pubkey, pubkey_bytes);
1069        assert!(our_decoded_relays.is_empty());
1070
1071        // Both should produce identical strings
1072        assert_eq!(our_nprofile, nostr_nprofile);
1073    }
1074
1075    #[test]
1076    fn nut_18_payment_request() {
1077        use nostr_sdk::prelude::*;
1078        let nprofile = "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5";
1079
1080        let nostr_decoded =
1081            Nip19Profile::from_bech32(nprofile).expect("nostr-sdk should decode our nprofile");
1082
1083        // Verify the decoded data can be re-encoded (round-trip works)
1084        let encoded = nostr_decoded.to_bech32().unwrap();
1085
1086        // Re-decode to verify content is preserved (encoding may differ due to normalization)
1087        let re_decoded = Nip19Profile::from_bech32(&encoded)
1088            .expect("nostr-sdk should decode re-encoded nprofile");
1089
1090        // Verify the semantic content is preserved
1091        assert_eq!(nostr_decoded.public_key, re_decoded.public_key);
1092        assert_eq!(nostr_decoded.relays.len(), re_decoded.relays.len());
1093    }
1094
1095    #[test]
1096    fn test_nostr_transport_with_nprofile_no_relays() {
1097        // Create a payment request with nostr transport using nprofile with empty relay list
1098        let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1099        let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
1100        let nprofile =
1101            PaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("encode nprofile");
1102
1103        let transport = Transport {
1104            _type: TransportType::Nostr,
1105            target: nprofile.clone(),
1106            tags: vec![vec!["n".to_string(), "17".to_string()]],
1107        };
1108
1109        let payment_request = PaymentRequest {
1110            payment_id: Some("nostr_test".to_string()),
1111            amount: Some(Amount::from(1000)),
1112            unit: Some(CurrencyUnit::Sat),
1113            single_use: None,
1114            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1115            description: Some("Nostr payment".to_string()),
1116            transports: vec![transport],
1117            nut10: None,
1118        };
1119
1120        let encoded = payment_request
1121            .to_bech32_string()
1122            .expect("encoding should work");
1123
1124        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1125
1126        assert_eq!(decoded.payment_id, payment_request.payment_id);
1127        assert_eq!(decoded.transports.len(), 1);
1128        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1129        assert!(decoded.transports[0].target.starts_with("nprofile"));
1130
1131        // Check that NIP-17 tag was preserved
1132        let tags = &decoded.transports[0].tags;
1133        assert!(tags
1134            .iter()
1135            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1136    }
1137
1138    #[test]
1139    fn test_nostr_transport_with_nprofile() {
1140        // Create a payment request with nostr transport using nprofile
1141        let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1142        let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
1143        let relays = vec!["wss://relay.example.com".to_string()];
1144        let nprofile =
1145            PaymentRequest::encode_nprofile(&pubkey_bytes, &relays).expect("encode nprofile");
1146
1147        let transport = Transport {
1148            _type: TransportType::Nostr,
1149            target: nprofile.clone(),
1150            tags: vec![vec!["n".to_string(), "17".to_string()]],
1151        };
1152
1153        let payment_request = PaymentRequest {
1154            payment_id: Some("nprofile_test".to_string()),
1155            amount: Some(Amount::from(2100)),
1156            unit: Some(CurrencyUnit::Sat),
1157            single_use: None,
1158            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1159            description: Some("Nostr payment with relays".to_string()),
1160            transports: vec![transport],
1161            nut10: None,
1162        };
1163
1164        let encoded = payment_request
1165            .to_bech32_string()
1166            .expect("encoding should work");
1167
1168        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1169
1170        assert_eq!(decoded.payment_id, payment_request.payment_id);
1171        assert_eq!(decoded.transports.len(), 1);
1172        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1173
1174        // Should be encoded back as nprofile since it has relays
1175        assert!(decoded.transports[0].target.starts_with("nprofile"));
1176
1177        // Check that relay was preserved in tags as "r" per NUT-26 spec
1178        let tags = &decoded.transports[0].tags;
1179        assert!(tags
1180            .iter()
1181            .any(|t| t.len() >= 2 && t[0] == "r" && t[1] == "wss://relay.example.com"));
1182    }
1183
1184    #[test]
1185    fn test_spec_example_nostr_transport() {
1186        // Test a complete example as specified in the spec:
1187        // Payment request with nostr transport, NIP-17, pubkey, and one relay
1188        let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1189        let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
1190        let relays = vec!["wss://relay.damus.io".to_string()];
1191        let nprofile =
1192            PaymentRequest::encode_nprofile(&pubkey_bytes, &relays).expect("encode nprofile");
1193
1194        let transport = Transport {
1195            _type: TransportType::Nostr,
1196            target: nprofile,
1197            tags: vec![vec!["n".to_string(), "17".to_string()]],
1198        };
1199
1200        let payment_request = PaymentRequest {
1201            payment_id: Some("spec_example".to_string()),
1202            amount: Some(Amount::from(10)),
1203            unit: Some(CurrencyUnit::Sat),
1204            single_use: Some(true),
1205            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1206            description: Some("Coffee".to_string()),
1207            transports: vec![transport],
1208            nut10: None,
1209        };
1210
1211        // Encode and decode
1212        let encoded = payment_request
1213            .to_bech32_string()
1214            .expect("encoding should work");
1215
1216        println!("Spec example encoded: {}", encoded);
1217
1218        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1219
1220        // Verify round-trip
1221        assert_eq!(decoded.payment_id, Some("spec_example".to_string()));
1222        assert_eq!(decoded.amount, Some(Amount::from(10)));
1223        assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1224        assert_eq!(decoded.single_use, Some(true));
1225        assert_eq!(decoded.description, Some("Coffee".to_string()));
1226        assert_eq!(decoded.transports.len(), 1);
1227        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1228
1229        // Verify relay and NIP are preserved
1230        let tags = &decoded.transports[0].tags;
1231        assert!(tags
1232            .iter()
1233            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1234        assert!(tags
1235            .iter()
1236            .any(|t| t.len() >= 2 && t[0] == "r" && t[1] == "wss://relay.damus.io"));
1237    }
1238
1239    #[test]
1240    fn test_decode_valid_bech32_with_nostr_pubkeys_and_mints() {
1241        // First, create a payment request with multiple mints and nostr transports with different pubkeys
1242        let pubkey1_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1243        let pubkey1_bytes = hex::decode(pubkey1_hex).unwrap();
1244        // Use nprofile with empty relay list instead of npub
1245        let nprofile1 =
1246            PaymentRequest::encode_nprofile(&pubkey1_bytes, &[]).expect("encode nprofile1");
1247
1248        let pubkey2_hex = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
1249        let pubkey2_bytes = hex::decode(pubkey2_hex).unwrap();
1250        let relays2 = vec![
1251            "wss://relay.damus.io".to_string(),
1252            "wss://nos.lol".to_string(),
1253        ];
1254        let nprofile2 =
1255            PaymentRequest::encode_nprofile(&pubkey2_bytes, &relays2).expect("encode nprofile2");
1256
1257        let transport1 = Transport {
1258            _type: TransportType::Nostr,
1259            target: nprofile1.clone(),
1260            tags: vec![vec!["n".to_string(), "17".to_string()]],
1261        };
1262
1263        let transport2 = Transport {
1264            _type: TransportType::Nostr,
1265            target: nprofile2.clone(),
1266            tags: vec![
1267                vec!["n".to_string(), "17".to_string()],
1268                vec!["n".to_string(), "44".to_string()],
1269            ],
1270        };
1271
1272        let payment_request = PaymentRequest {
1273            payment_id: Some("multi_test".to_string()),
1274            amount: Some(Amount::from(5000)),
1275            unit: Some(CurrencyUnit::Sat),
1276            single_use: Some(false),
1277            mints: vec![
1278                MintUrl::from_str("https://mint1.example.com").unwrap(),
1279                MintUrl::from_str("https://mint2.example.com").unwrap(),
1280                MintUrl::from_str("https://testnut.cashu.space").unwrap(),
1281            ],
1282            description: Some("Payment with multiple transports and mints".to_string()),
1283            transports: vec![transport1, transport2],
1284            nut10: None,
1285        };
1286
1287        // Encode to bech32 string
1288        let encoded = payment_request
1289            .to_bech32_string()
1290            .expect("encoding should work");
1291
1292        println!("Encoded payment request: {}", encoded);
1293
1294        // Now decode the bech32 string and verify contents
1295        let decoded = PaymentRequest::from_bech32_string(&encoded)
1296            .expect("should decode valid bech32 string");
1297
1298        // Verify basic fields
1299        assert_eq!(decoded.payment_id, Some("multi_test".to_string()));
1300        assert_eq!(decoded.amount, Some(Amount::from(5000)));
1301        assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1302        assert_eq!(decoded.single_use, Some(false));
1303        assert_eq!(
1304            decoded.description,
1305            Some("Payment with multiple transports and mints".to_string())
1306        );
1307
1308        // Verify mints
1309        let mints = &decoded.mints;
1310        assert_eq!(mints.len(), 3);
1311
1312        // MintUrl normalizes URLs and may add trailing slashes
1313        let mint_strings: Vec<String> =
1314            mints.iter().map(std::string::ToString::to_string).collect();
1315        assert!(
1316            mint_strings[0] == "https://mint1.example.com/"
1317                || mint_strings[0] == "https://mint1.example.com"
1318        );
1319        assert!(
1320            mint_strings[1] == "https://mint2.example.com/"
1321                || mint_strings[1] == "https://mint2.example.com"
1322        );
1323        assert!(
1324            mint_strings[2] == "https://testnut.cashu.space/"
1325                || mint_strings[2] == "https://testnut.cashu.space"
1326        );
1327
1328        // Verify transports
1329        assert_eq!(decoded.transports.len(), 2);
1330
1331        // Verify first transport (nprofile with no relays)
1332        let transport1_decoded = &decoded.transports[0];
1333        assert_eq!(transport1_decoded._type, TransportType::Nostr);
1334        assert!(transport1_decoded.target.starts_with("nprofile"));
1335
1336        // Decode the nprofile to verify the pubkey
1337        let (decoded_pubkey1, decoded_relays1) =
1338            PaymentRequest::decode_nprofile(&transport1_decoded.target)
1339                .expect("should decode nprofile");
1340        assert_eq!(decoded_pubkey1, pubkey1_bytes);
1341        assert!(decoded_relays1.is_empty());
1342
1343        // Verify NIP-17 tag
1344        let tags1 = &transport1_decoded.tags;
1345        assert!(tags1
1346            .iter()
1347            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1348
1349        // Verify second transport (nprofile)
1350        let transport2_decoded = &decoded.transports[1];
1351        assert_eq!(transport2_decoded._type, TransportType::Nostr);
1352        assert!(transport2_decoded.target.starts_with("nprofile"));
1353
1354        // Decode the nprofile to verify the pubkey and relays
1355        let (decoded_pubkey2, decoded_relays2) =
1356            PaymentRequest::decode_nprofile(&transport2_decoded.target)
1357                .expect("should decode nprofile");
1358        assert_eq!(decoded_pubkey2, pubkey2_bytes);
1359        assert_eq!(decoded_relays2, relays2);
1360
1361        // Verify tags include both NIPs and relays
1362        let tags2 = &transport2_decoded.tags;
1363        assert!(tags2
1364            .iter()
1365            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1366        assert!(tags2
1367            .iter()
1368            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "44"));
1369        assert!(tags2
1370            .iter()
1371            .any(|t| t.len() >= 2 && t[0] == "r" && t[1] == "wss://relay.damus.io"));
1372        assert!(tags2
1373            .iter()
1374            .any(|t| t.len() >= 2 && t[0] == "r" && t[1] == "wss://nos.lol"));
1375    }
1376
1377    // Test vectors from NUT-26 specification
1378    // https://github.com/cashubtc/nuts/blob/main/tests/26-tests.md
1379    #[test]
1380    fn test_basic_payment_request() {
1381        // Basic payment request with required fields
1382        let json = r#"{
1383            "i": "b7a90176",
1384            "a": 10,
1385            "u": "sat",
1386            "m": ["https://8333.space:3338"],
1387            "t": [
1388                {
1389                    "t": "nostr",
1390                    "a": "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n",
1391                    "g": [["n", "17"]]
1392                }
1393            ]
1394        }"#;
1395
1396        let expected_encoded = "CREQB1QYQQSC3HVYUNQVFHXCPQQZQQQQQQQQQQQQ9QXQQPQQZSQ9MGW368QUE69UHNSVENXVH8XURPVDJN5VENXVUQWQREQYQQZQQZQQSGM6QFA3C8DTZ2FVZHVFQEACMWM0E50PE3K5TFMVPJJMN0VJ7M2TGRQQZSZMSZXYMSXQQHQ9EPGAMNWVAZ7TMJV4KXZ7FWV3SK6ATN9E5K7QCQRGQHY9MHWDEN5TE0WFJKCCTE9CURXVEN9EEHQCTRV5HSXQQSQ9EQ6AMNWVAZ7TMWDAEJUMR0DSRYDPGF";
1397
1398        // Parse the JSON into a PaymentRequest
1399        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1400        let payment_request_cloned = payment_request.clone();
1401
1402        // Verify the payment request fields
1403        assert_eq!(
1404            payment_request_cloned.payment_id.as_ref().unwrap(),
1405            "b7a90176"
1406        );
1407        assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
1408        assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
1409        assert_eq!(
1410            payment_request_cloned.mints,
1411            vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
1412        );
1413
1414        let transport = payment_request.transports.first().unwrap();
1415        assert_eq!(transport._type, TransportType::Nostr);
1416        assert_eq!(transport.target, "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n");
1417        assert_eq!(
1418            transport.tags,
1419            vec![vec!["n".to_string(), "17".to_string()]]
1420        );
1421
1422        // Test bech32m encoding (CREQ-B format) - this is what NUT-26 is about
1423        let encoded = payment_request
1424            .to_bech32_string()
1425            .expect("Failed to encode to bech32");
1426
1427        // Verify it starts with CREQB1 (uppercase because we use encode_upper)
1428        assert!(encoded.starts_with("CREQB1"));
1429
1430        // Verify exact encoding matches expected
1431        assert_eq!(encoded, expected_encoded);
1432
1433        // Test round-trip via bech32 format
1434        let decoded = PaymentRequest::from_bech32_string(&encoded).unwrap();
1435
1436        // Verify decoded fields match original
1437        assert_eq!(decoded.payment_id.as_ref().unwrap(), "b7a90176");
1438        assert_eq!(decoded.amount.unwrap(), Amount::from(10));
1439        assert_eq!(decoded.unit.unwrap(), CurrencyUnit::Sat);
1440        assert_eq!(
1441            decoded.mints,
1442            vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
1443        );
1444
1445        // Verify transport type and that it has the NIP-17 tag
1446        assert_eq!(decoded.transports.len(), 1);
1447        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1448        let tags = &decoded.transports[0].tags;
1449        assert!(tags
1450            .iter()
1451            .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1452
1453        // Verify the pubkey is preserved (decode both nprofiles and compare pubkeys)
1454        let (original_pubkey, _) = PaymentRequest::decode_nprofile(&transport.target).unwrap();
1455        let (decoded_pubkey, _) =
1456            PaymentRequest::decode_nprofile(&decoded.transports[0].target).unwrap();
1457        assert_eq!(original_pubkey, decoded_pubkey);
1458
1459        // Test decoding the expected encoded string
1460        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1461        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
1462    }
1463
1464    #[test]
1465    fn test_nostr_transport_payment_request() {
1466        // Nostr transport payment request with multiple mints
1467        let json = r#"{
1468            "i": "f92a51b8",
1469            "a": 100,
1470            "u": "sat",
1471            "m": ["https://mint1.example.com", "https://mint2.example.com"],
1472            "t": [
1473                {
1474                    "t": "nostr",
1475                    "a": "nprofile1qqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8uzqt",
1476                    "g": [["n", "17"], ["n", "9735"]]
1477                }
1478            ]
1479        }"#;
1480
1481        let expected_encoded = "CREQB1QYQQSE3EXFSN2VTZ8QPQQZQQQQQQQQQQQPJQXQQPQQZSQXTGW368QUE69UHK66TWWSCJUETCV9KHQMR99E3K7MG9QQVKSAR5WPEN5TE0D45KUAPJ9EJHSCTDWPKX2TNRDAKSWQPEQYQQZQQZQQSQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQRQQZSZMSZXYMSXQQ8Q9HQGWFHXV6SCAGZ48";
1482
1483        // Parse the JSON into a PaymentRequest
1484        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1485        let payment_request_cloned = payment_request.clone();
1486
1487        // Verify the payment request fields
1488        assert_eq!(
1489            payment_request_cloned.payment_id.as_ref().unwrap(),
1490            "f92a51b8"
1491        );
1492        assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
1493        assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
1494        assert_eq!(
1495            payment_request_cloned.mints,
1496            vec![
1497                MintUrl::from_str("https://mint1.example.com").unwrap(),
1498                MintUrl::from_str("https://mint2.example.com").unwrap()
1499            ]
1500        );
1501
1502        let transport = payment_request_cloned.transports.first().unwrap();
1503        assert_eq!(transport._type, TransportType::Nostr);
1504        assert_eq!(
1505            transport.target,
1506            "nprofile1qqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8uzqt"
1507        );
1508        assert_eq!(
1509            transport.tags,
1510            vec![
1511                vec!["n".to_string(), "17".to_string()],
1512                vec!["n".to_string(), "9735".to_string()]
1513            ]
1514        );
1515
1516        // Test round-trip serialization
1517        let encoded = payment_request.to_bech32_string().unwrap();
1518
1519        // Verify exact encoding matches expected
1520        assert_eq!(encoded, expected_encoded);
1521
1522        let decoded = PaymentRequest::from_str(&encoded).unwrap();
1523        assert_eq!(payment_request, decoded);
1524
1525        // Test decoding the expected encoded string
1526        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1527        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
1528    }
1529
1530    #[test]
1531    fn test_minimal_payment_request() {
1532        // Minimal payment request with only required fields
1533        let json = r#"{
1534            "i": "7f4a2b39",
1535            "u": "sat",
1536            "m": ["https://mint.example.com"]
1537        }"#;
1538
1539        let expected_encoded =
1540            "CREQB1QYQQSDMXX3SNYC3N8YPSQQGQQ5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKSYP0LHG";
1541
1542        // Parse the JSON into a PaymentRequest
1543        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1544        let payment_request_cloned = payment_request.clone();
1545
1546        // Verify the payment request fields
1547        assert_eq!(
1548            payment_request_cloned.payment_id.as_ref().unwrap(),
1549            "7f4a2b39"
1550        );
1551        assert_eq!(payment_request_cloned.amount, None);
1552        assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
1553        assert_eq!(
1554            payment_request_cloned.mints,
1555            vec![MintUrl::from_str("https://mint.example.com").unwrap()]
1556        );
1557        assert_eq!(payment_request_cloned.transports, vec![]);
1558
1559        // Test round-trip serialization
1560        let encoded = payment_request.to_bech32_string().unwrap();
1561        assert_eq!(encoded, expected_encoded);
1562        let decoded = PaymentRequest::from_bech32_string(&encoded).unwrap();
1563        assert_eq!(payment_request, decoded);
1564
1565        // Test decoding the expected encoded string
1566        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1567        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
1568    }
1569
1570    #[test]
1571    fn test_nut10_locking_payment_request() {
1572        // Payment request with NUT-10 P2PK locking
1573        let json = r#"{
1574            "i": "c9e45d2a",
1575            "a": 500,
1576            "u": "sat",
1577            "m": ["https://mint.example.com"],
1578            "nut10": {
1579                "k": "P2PK",
1580                "d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
1581                "t": [["timeout", "3600"]]
1582            }
1583        }"#;
1584
1585        let expected_encoded = "CREQB1QYQQSCEEV56R2EPJVYPQQZQQQQQQQQQQQ86QXQQPQQZSQXRGW368QUE69UHK66TWWSHX27RPD4CXCEFWVDHK6ZQQTYQSQQGQQGQYYVPJVVEKYDTZVGERWEFNXCCNGDFHVVUNYEPEXDJRWWRYVSMNXEPNVS6NXDENXGCNZVRZXF3KVEFCVG6NQENZVVCXZCNRXCCN2EFEVVENXVGRQQXSWARFD4JK7AT5QSENVVPS2N5FAS";
1586
1587        // Parse the JSON into a PaymentRequest
1588        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1589        let payment_request_cloned = payment_request.clone();
1590
1591        // Verify the payment request fields
1592        assert_eq!(
1593            payment_request_cloned.payment_id.as_ref().unwrap(),
1594            "c9e45d2a"
1595        );
1596        assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
1597        assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
1598        assert_eq!(
1599            payment_request_cloned.mints,
1600            vec![MintUrl::from_str("https://mint.example.com").unwrap()]
1601        );
1602
1603        // Test NUT-10 locking
1604        let nut10 = payment_request_cloned.nut10.unwrap();
1605        assert_eq!(nut10.kind, Kind::P2PK);
1606        assert_eq!(
1607            nut10.data,
1608            "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
1609        );
1610        assert_eq!(
1611            nut10.tags,
1612            Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
1613        );
1614
1615        // Test round-trip serialization
1616        let encoded = payment_request.to_bech32_string().unwrap();
1617        assert_eq!(encoded, expected_encoded);
1618        let decoded = PaymentRequest::from_str(&encoded).unwrap();
1619        assert_eq!(payment_request, decoded);
1620
1621        // Test decoding the expected encoded string
1622        let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
1623        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
1624    }
1625
1626    #[test]
1627    fn test_nut26_example() {
1628        // Payment request with NUT-10 P2PK locking
1629        let json = r#"{
1630  "i": "demo123",
1631  "a": 1000,
1632  "u": "sat",
1633  "s": true,
1634  "m": ["https://mint.example.com"],
1635  "d": "Coffee payment"
1636}"#;
1637
1638        let expected_encoded = "CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ";
1639
1640        // Parse the JSON into a PaymentRequest
1641        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1642
1643        let encoded = payment_request.to_bech32_string().unwrap();
1644
1645        assert_eq!(expected_encoded, encoded);
1646    }
1647
1648    #[test]
1649    fn test_http_post_transport_kind_1() {
1650        // Test HTTP POST transport (kind=0x01) encoding and decoding
1651        let json = r#"{
1652            "i": "http_test",
1653            "a": 250,
1654            "u": "sat",
1655            "m": ["https://mint.example.com"],
1656            "t": [
1657                {
1658                    "t": "post",
1659                    "a": "https://api.example.com/v1/payment",
1660                    "g": [["custom", "value1", "value2"]]
1661                }
1662            ]
1663        }"#;
1664
1665        // Note: The encoded string is generated by our implementation and verified via round-trip
1666        let expected_encoded = "CREQB1QYQQJ6R5W3C97AR9WD6QYQQGQQQQQQQQQQQ05QCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MG8QPQSZQQPQYPQQGNGW368QUE69UHKZURF9EJHSCTDWPKX2TNRDAKJ7A339ACXZ7TDV4H8GQCQZ5RXXATNW3HK6PNKV9K82EF3QEMXZMR4V5EQ9X3SJM";
1667
1668        // Parse the JSON into a PaymentRequest
1669        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1670
1671        let encoded = payment_request
1672            .to_bech32_string()
1673            .expect("encoding should work");
1674
1675        // Verify exact encoding matches expected
1676        assert_eq!(encoded, expected_encoded);
1677
1678        // Decode and verify round-trip
1679        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1680
1681        // Verify transport type is HTTP POST
1682        assert_eq!(decoded.transports.len(), 1);
1683        assert_eq!(decoded.transports[0]._type, TransportType::HttpPost);
1684        assert_eq!(
1685            decoded.transports[0].target,
1686            "https://api.example.com/v1/payment"
1687        );
1688
1689        // Verify custom tags are preserved
1690        let tags = &decoded.transports[0].tags;
1691        assert!(tags
1692            .iter()
1693            .any(|t| t.len() >= 3 && t[0] == "custom" && t[1] == "value1" && t[2] == "value2"));
1694
1695        // Test decoding the expected encoded string
1696        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1697        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "http_test");
1698    }
1699
1700    #[test]
1701    fn test_relay_tag_extraction_from_nprofile() {
1702        // Test that relays are properly extracted from nprofile as "r" tags per NUT-26 spec
1703        let json = r#"{
1704            "i": "relay_test",
1705            "a": 100,
1706            "u": "sat",
1707            "m": ["https://mint.example.com"],
1708            "t": [
1709                {
1710                    "t": "nostr",
1711                    "a": "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gprpmhxue69uhhyetvv9unztn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5q3samnwvaz7tmjv4kxz7fn9ejhsctdwpkx2tnrdaksxzjpjp"
1712                }
1713            ]
1714        }"#;
1715
1716        let expected_encoded = "CREQB1QYQQ5UN9D3SHJHM5V4EHGQSQPQQQQQQQQQQQQEQRQQQSQPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDQUQGZQGQQYQQYQPQ80CVV07TJDRRGPA0J7J7TMNYL2YR6YR7L8J4S3EVF6U64TH6GKWSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7F39EJHSCTDWPKX2TNRDAKSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7FJ9EJHSCTDWPKX2TNRDAKSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7FN9EJHSCTDWPKX2TNRDAKSKRFDAR";
1717
1718        // Parse the JSON into a PaymentRequest
1719        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1720
1721        let encoded = payment_request
1722            .to_bech32_string()
1723            .expect("encoding should work");
1724
1725        // Verify exact encoding matches expected
1726        assert_eq!(encoded, expected_encoded);
1727
1728        // Decode and verify round-trip
1729        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1730
1731        // Verify relays were extracted as "r" tags per NUT-26 spec
1732        let tags = &decoded.transports[0].tags;
1733
1734        // Check all three relays are present as "r" tags per NUT-26 spec
1735        let relay_tags: Vec<&Vec<String>> = tags
1736            .iter()
1737            .filter(|t| !t.is_empty() && t[0] == "r")
1738            .collect();
1739        assert_eq!(relay_tags.len(), 3);
1740
1741        let relay_values: Vec<&str> = relay_tags
1742            .iter()
1743            .filter(|t| t.len() >= 2)
1744            .map(|t| t[1].as_str())
1745            .collect();
1746        // The nprofile has 3 relays embedded - verified by decode
1747        assert_eq!(relay_values.len(), 3);
1748
1749        // Verify the nprofile is preserved (relays are encoded back into it)
1750        assert_eq!(
1751            "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gprpmhxue69uhhyetvv9unztn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5q3samnwvaz7tmjv4kxz7fn9ejhsctdwpkx2tnrdaksxzjpjp",
1752            decoded.transports[0].target
1753        );
1754
1755        // Also verify the nprofile contains the relays
1756        let (_, decoded_relays) =
1757            PaymentRequest::decode_nprofile(&decoded.transports[0].target).unwrap();
1758        assert_eq!(decoded_relays.len(), 3);
1759
1760        // Test decoding the expected encoded string
1761        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1762        assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "relay_test");
1763    }
1764
1765    #[test]
1766    fn test_description_field() {
1767        // Test description field (tag 0x06) encoding and decoding
1768        let expected_encoded = "CREQB1QYQQJER9WD347AR9WD6QYQQGQQQQQQQQQQQXGQCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MGXQQV9GETNWSS8QCTED4JKUAPQV3JHXCMJD9C8G6T0DCFLJJRX";
1769
1770        let payment_request = PaymentRequest {
1771            payment_id: Some("desc_test".to_string()),
1772            amount: Some(Amount::from(100)),
1773            unit: Some(CurrencyUnit::Sat),
1774            single_use: None,
1775            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1776            description: Some("Test payment description".to_string()),
1777            transports: vec![],
1778            nut10: None,
1779        };
1780
1781        let encoded = payment_request
1782            .to_bech32_string()
1783            .expect("encoding should work");
1784
1785        assert_eq!(encoded, expected_encoded);
1786
1787        // Decode from the expected encoded string
1788        let decoded =
1789            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
1790
1791        assert_eq!(
1792            decoded.description,
1793            Some("Test payment description".to_string())
1794        );
1795        assert_eq!(decoded.payment_id, Some("desc_test".to_string()));
1796        assert_eq!(decoded.amount, Some(Amount::from(100)));
1797        assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1798    }
1799
1800    #[test]
1801    fn test_single_use_field_true() {
1802        // Test single_use field (tag 0x04) with value true
1803        let expected_encoded = "CREQB1QYQQ7UMFDENKCE2LW4EK2HM5WF6K2QSQPQQQQQQQQQQQQEQRQQQSQPQQQYQS2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MGX0AYM7";
1804
1805        let payment_request = PaymentRequest {
1806            payment_id: Some("single_use_true".to_string()),
1807            amount: Some(Amount::from(100)),
1808            unit: Some(CurrencyUnit::Sat),
1809            single_use: Some(true),
1810            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1811            description: None,
1812            transports: vec![],
1813            nut10: None,
1814        };
1815
1816        let encoded = payment_request
1817            .to_bech32_string()
1818            .expect("encoding should work");
1819
1820        assert_eq!(encoded, expected_encoded);
1821
1822        // Decode from the expected encoded string
1823        let decoded =
1824            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
1825
1826        assert_eq!(decoded.single_use, Some(true));
1827        assert_eq!(decoded.payment_id, Some("single_use_true".to_string()));
1828    }
1829
1830    #[test]
1831    fn test_single_use_field_false() {
1832        // Test single_use field (tag 0x04) with value false
1833        let expected_encoded = "CREQB1QYQPQUMFDENKCE2LW4EK2HMXV9K8XEGZQQYQQQQQQQQQQQRYQVQQZQQYQQQSQPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDQ40L90";
1834
1835        let payment_request = PaymentRequest {
1836            payment_id: Some("single_use_false".to_string()),
1837            amount: Some(Amount::from(100)),
1838            unit: Some(CurrencyUnit::Sat),
1839            single_use: Some(false),
1840            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1841            description: None,
1842            transports: vec![],
1843            nut10: None,
1844        };
1845
1846        let encoded = payment_request
1847            .to_bech32_string()
1848            .expect("encoding should work");
1849
1850        assert_eq!(encoded, expected_encoded);
1851
1852        // Decode from the expected encoded string
1853        let decoded =
1854            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
1855
1856        assert_eq!(decoded.single_use, Some(false));
1857        assert_eq!(decoded.payment_id, Some("single_use_false".to_string()));
1858    }
1859
1860    #[test]
1861    fn test_unit_msat() {
1862        // Test msat unit encoding (should be string, not 0x00)
1863        let expected_encoded = "CREQB1QYQQJATWD9697MTNV96QYQQGQQQQQQQQQQP7SQCQQ3KHXCT5Q5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKSYYMU95";
1864
1865        let payment_request = PaymentRequest {
1866            payment_id: Some("unit_msat".to_string()),
1867            amount: Some(Amount::from(1000)),
1868            unit: Some(CurrencyUnit::Msat),
1869            single_use: None,
1870            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1871            description: None,
1872            transports: vec![],
1873            nut10: None,
1874        };
1875
1876        let encoded = payment_request
1877            .to_bech32_string()
1878            .expect("encoding should work");
1879
1880        assert_eq!(encoded, expected_encoded);
1881
1882        // Decode from the expected encoded string
1883        let decoded =
1884            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
1885
1886        assert_eq!(decoded.unit, Some(CurrencyUnit::Msat));
1887        assert_eq!(decoded.payment_id, Some("unit_msat".to_string()));
1888        assert_eq!(decoded.amount, Some(Amount::from(1000)));
1889    }
1890
1891    #[test]
1892    fn test_unit_usd() {
1893        // Test usd unit encoding (should be string, not 0x00)
1894        let expected_encoded = "CREQB1QYQQSATWD9697ATNVSPQQZQQQQQQQQQQQ86QXQQRW4EKGPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDEPCJYC";
1895
1896        let payment_request = PaymentRequest {
1897            payment_id: Some("unit_usd".to_string()),
1898            amount: Some(Amount::from(500)),
1899            unit: Some(CurrencyUnit::Usd),
1900            single_use: None,
1901            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
1902            description: None,
1903            transports: vec![],
1904            nut10: None,
1905        };
1906
1907        let encoded = payment_request
1908            .to_bech32_string()
1909            .expect("encoding should work");
1910
1911        assert_eq!(encoded, expected_encoded);
1912
1913        // Decode from the expected encoded string
1914        let decoded =
1915            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
1916
1917        assert_eq!(decoded.unit, Some(CurrencyUnit::Usd));
1918        assert_eq!(decoded.payment_id, Some("unit_usd".to_string()));
1919        assert_eq!(decoded.amount, Some(Amount::from(500)));
1920    }
1921
1922    #[test]
1923    fn test_multiple_transports() {
1924        // Test payment request with multiple transport options (priority order)
1925        let json = r#"{
1926            "i": "multi_transport",
1927            "a": 500,
1928            "u": "sat",
1929            "m": ["https://mint.example.com"],
1930            "d": "Payment with multiple transports",
1931            "t": [
1932                {
1933                    "t": "nostr",
1934                    "a": "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8g2lcy6q",
1935                    "g": [["n", "17"]]
1936                },
1937                {
1938                    "t": "post",
1939                    "a": "https://api1.example.com/payment"
1940                },
1941                {
1942                    "t": "post",
1943                    "a": "https://api2.example.com/payment",
1944                    "g": [["priority", "backup"]]
1945                }
1946            ]
1947        }"#;
1948
1949        let expected_encoded = "CREQB1QYQQ7MT4D36XJHM5WFSKUUMSDAE8GQSQPQQQQQQQQQQQRAQRQQQSQPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDQCQZQ5RP09KK2MN5YPMKJARGYPKH2MR5D9CXCEFQW3EXZMNNWPHHYARNQUQZ7QGQQYQQYQPQ80CVV07TJDRRGPA0J7J7TMNYL2YR6YR7L8J4S3EVF6U64TH6GKWSXQQ9Q9HQYVFHQUQZWQGQQYQSYQPQDP68GURN8GHJ7CTSDYCJUETCV9KHQMR99E3K7MF0WPSHJMT9DE6QWQP6QYQQZQGZQQSXSAR5WPEN5TE0V9CXJV3WV4UXZMTSD3JJUCM0D5HHQCTED4JKUAQRQQGQSURJD9HHY6T50YRXYCTRDD6HQTSH7TP";
1950
1951        // Parse the JSON into a PaymentRequest
1952        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1953
1954        let encoded = payment_request
1955            .to_bech32_string()
1956            .expect("encoding should work");
1957
1958        // Verify exact encoding matches expected
1959        assert_eq!(encoded, expected_encoded);
1960
1961        // Decode from the encoded string
1962        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1963
1964        // Verify all three transports are preserved in order
1965        assert_eq!(decoded.transports.len(), 3);
1966
1967        // First transport: Nostr
1968        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1969        assert!(decoded.transports[0].target.starts_with("nprofile"));
1970
1971        // Second transport: HTTP POST
1972        assert_eq!(decoded.transports[1]._type, TransportType::HttpPost);
1973        assert_eq!(
1974            decoded.transports[1].target,
1975            "https://api1.example.com/payment"
1976        );
1977
1978        // Third transport: HTTP POST with tags
1979        assert_eq!(decoded.transports[2]._type, TransportType::HttpPost);
1980        assert_eq!(
1981            decoded.transports[2].target,
1982            "https://api2.example.com/payment"
1983        );
1984        let tags = &decoded.transports[2].tags;
1985        assert!(tags
1986            .iter()
1987            .any(|t| t.len() >= 2 && t[0] == "priority" && t[1] == "backup"));
1988
1989        // Test decoding the expected encoded string
1990        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
1991        assert_eq!(
1992            decoded_from_spec.payment_id.as_ref().unwrap(),
1993            "multi_transport"
1994        );
1995    }
1996
1997    #[test]
1998    fn test_minimal_transport_nostr_only_pubkey() {
1999        // Test minimal Nostr transport with just pubkey (no relays, no tags)
2000        let json = r#"{
2001            "i": "minimal_nostr",
2002            "u": "sat",
2003            "m": ["https://mint.example.com"],
2004            "t": [
2005                {
2006                    "t": "nostr",
2007                    "a": "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8g2lcy6q"
2008                }
2009            ]
2010        }"#;
2011
2012        let expected_encoded = "CREQB1QYQQ6MTFDE5K6CTVTAHX7UM5WGPSQQGQQ5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKSWQP8QYQQZQQZQQSRHUXX8L9EX335Q7HE0F09AEJ04ZPAZPL0NE2CGUKYAWD24MAYT8G7QNXMQ";
2013
2014        // Parse the JSON into a PaymentRequest
2015        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
2016
2017        let encoded = payment_request
2018            .to_bech32_string()
2019            .expect("encoding should work");
2020
2021        // Verify exact encoding matches expected
2022        assert_eq!(encoded, expected_encoded);
2023
2024        // Decode from the encoded string
2025        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2026
2027        assert_eq!(decoded.transports.len(), 1);
2028        assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
2029        assert!(decoded.transports[0].target.starts_with("nprofile"));
2030
2031        // Tags should be None for minimal transport
2032        assert!(decoded.transports[0].tags.is_empty());
2033
2034        // Test decoding the expected encoded string
2035        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
2036        assert_eq!(
2037            decoded_from_spec.payment_id.as_ref().unwrap(),
2038            "minimal_nostr"
2039        );
2040    }
2041
2042    #[test]
2043    fn test_minimal_transport_http_just_url() {
2044        // Test minimal HTTP POST transport with just URL (no tags)
2045        let json = r#"{
2046            "i": "minimal_http",
2047            "u": "sat",
2048            "m": ["https://mint.example.com"],
2049            "t": [
2050                {
2051                    "t": "post",
2052                    "a": "https://api.example.com"
2053                }
2054            ]
2055        }"#;
2056
2057        let expected_encoded = "CREQB1QYQQCMTFDE5K6CTVTA58GARSQVQQZQQ9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RSQ8SPQQQSZQSQZA58GARSWVAZ7TMPWP5JUETCV9KHQMR99E3K7MG0TWYGX";
2058
2059        // Parse the JSON into a PaymentRequest
2060        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
2061
2062        let encoded = payment_request
2063            .to_bech32_string()
2064            .expect("encoding should work");
2065
2066        // Verify exact encoding matches expected
2067        assert_eq!(encoded, expected_encoded);
2068
2069        // Decode and verify round-trip
2070        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2071
2072        assert_eq!(decoded.transports.len(), 1);
2073        assert_eq!(decoded.transports[0]._type, TransportType::HttpPost);
2074        assert_eq!(decoded.transports[0].target, "https://api.example.com");
2075        assert!(decoded.transports[0].tags.is_empty());
2076
2077        // Test decoding the expected encoded string
2078        let decoded_from_spec = PaymentRequest::from_bech32_string(expected_encoded).unwrap();
2079        assert_eq!(
2080            decoded_from_spec.payment_id.as_ref().unwrap(),
2081            "minimal_http"
2082        );
2083    }
2084
2085    #[test]
2086    fn test_in_band_transport_implicit() {
2087        // Test that in-band transport is represented by absence of transport tag
2088        // Per NUT-26: in-band transport means no transport entries in the list
2089
2090        let payment_request = PaymentRequest {
2091            payment_id: Some("in_band_test".to_string()),
2092            amount: Some(Amount::from(100)),
2093            unit: Some(CurrencyUnit::Sat),
2094            single_use: None,
2095            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
2096            description: None,
2097            transports: vec![], // Empty transports = in-band per NUT-26
2098            nut10: None,
2099        };
2100
2101        let encoded = payment_request
2102            .to_bech32_string()
2103            .expect("encoding should work");
2104
2105        // Decode the encoded string
2106        let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2107
2108        // Empty transports list means in-band transport per NUT-26
2109        assert_eq!(decoded.transports.len(), 0);
2110        assert_eq!(decoded.payment_id, Some("in_band_test".to_string()));
2111        assert_eq!(decoded.amount, Some(Amount::from(100)));
2112    }
2113
2114    #[test]
2115    fn test_nut10_htlc_kind_1() {
2116        // Test NUT-10 HTLC (kind=1) encoding and decoding
2117        let json = r#"{
2118            "i": "htlc_test",
2119            "a": 1000,
2120            "u": "sat",
2121            "m": ["https://mint.example.com"],
2122            "d": "HTLC locked payment",
2123            "nut10": {
2124                "k": "HTLC",
2125                "d": "a]0e66820bfb412212cf7ab3deb0459ce282a1b04fda76ea6026a67e41ae26f3dc",
2126                "t": [
2127                    ["locktime", "1700000000"],
2128                    ["refund", "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e"]
2129                ]
2130            }
2131        }"#;
2132
2133        // Note: The encoded string is generated by our implementation and verified via round-trip
2134        let expected_encoded = "CREQB1QYQQJ6R5D3347AR9WD6QYQQGQQQQQQQQQQP7SQCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MGXQQF5S4ZVGVSXCMMRDDJKGGRSV9UK6ETWWSYQPTGPQQQSZQSQGFS46VR9XCMRSV3SVFNXYDP3XGERZVNRVCMKZC3NV3JKYVP5X5UKXEFJ8QEXZVTZXQ6XVERPXUMX2CFKXQERVCFKXAJNGVTPV5ERVE3NV33SXQQ5PPKX7CMTW35K6EG2XYMNQVPSXQCRQVPSQVQY5PNJV4N82MNYGGCRXVEJ8QCKXVEHXCMNWETPXGMNXETZXUCNSVMZXUURXVPKXANR2V35XSUNXVM9VCMNSEPCVVEKVVF4VGCKZDEHVD3RYDPKXQUNJCEJXEJS4EHJHC";
2135
2136        // Parse the JSON into a PaymentRequest
2137        let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
2138
2139        let encoded = payment_request
2140            .to_bech32_string()
2141            .expect("encoding should work");
2142
2143        // Verify exact encoding matches expected
2144        assert_eq!(encoded, expected_encoded);
2145
2146        // Decode from the encoded string and verify round-trip
2147        let decoded =
2148            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
2149
2150        // Verify all top-level fields
2151        assert_eq!(decoded.payment_id, Some("htlc_test".to_string()));
2152        assert_eq!(decoded.amount, Some(Amount::from(1000)));
2153        assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
2154        assert_eq!(
2155            decoded.mints,
2156            vec![MintUrl::from_str("https://mint.example.com").unwrap()]
2157        );
2158        assert_eq!(decoded.description, Some("HTLC locked payment".to_string()));
2159
2160        // Verify NUT-10 fields
2161        let nut10 = decoded.nut10.as_ref().unwrap();
2162        assert_eq!(nut10.kind, Kind::HTLC);
2163        assert_eq!(
2164            nut10.data,
2165            "a]0e66820bfb412212cf7ab3deb0459ce282a1b04fda76ea6026a67e41ae26f3dc"
2166        );
2167
2168        // Verify all tags with exact values
2169        let tags = nut10.tags.clone().unwrap();
2170        assert_eq!(tags.len(), 2);
2171        assert_eq!(
2172            tags[0],
2173            vec!["locktime".to_string(), "1700000000".to_string()]
2174        );
2175        assert_eq!(
2176            tags[1],
2177            vec![
2178                "refund".to_string(),
2179                "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e".to_string()
2180            ]
2181        );
2182    }
2183
2184    #[test]
2185    fn test_case_insensitive_decoding() {
2186        // Test that decoder accepts both lowercase and uppercase input
2187        // Note: Per BIP-173/BIP-350, mixed-case is NOT valid for bech32/bech32m
2188        // "Decoders MUST NOT accept strings where some characters are uppercase and some are lowercase"
2189        let payment_request = PaymentRequest {
2190            payment_id: Some("case_test".to_string()),
2191            amount: Some(Amount::from(100)),
2192            unit: Some(CurrencyUnit::Sat),
2193            single_use: None,
2194            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
2195            description: None,
2196            transports: vec![],
2197            nut10: None,
2198        };
2199
2200        let uppercase = payment_request
2201            .to_bech32_string()
2202            .expect("encoding should work");
2203
2204        // Convert to lowercase
2205        let lowercase = uppercase.to_lowercase();
2206
2207        // Both uppercase and lowercase should decode successfully
2208        let decoded_upper =
2209            PaymentRequest::from_bech32_string(&uppercase).expect("uppercase should decode");
2210        let decoded_lower =
2211            PaymentRequest::from_bech32_string(&lowercase).expect("lowercase should decode");
2212
2213        // Both should produce the same result
2214        assert_eq!(decoded_upper.payment_id, Some("case_test".to_string()));
2215        assert_eq!(decoded_lower.payment_id, Some("case_test".to_string()));
2216
2217        assert_eq!(decoded_upper.amount, decoded_lower.amount);
2218        assert_eq!(decoded_upper.unit, decoded_lower.unit);
2219    }
2220
2221    #[test]
2222    fn test_custom_currency_unit() {
2223        // Test that custom/unknown currency units are preserved
2224        let expected_encoded = "CREQB1QYQQKCM4WD6X7M2LW4HXJAQZQQYQQQQQQQQQQQRYQVQQXCN5VVZSQXRGW368QUE69UHK66TWWSHX27RPD4CXCEFWVDHK6PZHCW8";
2225
2226        let payment_request = PaymentRequest {
2227            payment_id: Some("custom_unit".to_string()),
2228            amount: Some(Amount::from(100)),
2229            unit: Some(CurrencyUnit::Custom("btc".to_string())),
2230            single_use: None,
2231            mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
2232            description: None,
2233            transports: vec![],
2234            nut10: None,
2235        };
2236
2237        let encoded = payment_request
2238            .to_bech32_string()
2239            .expect("encoding should work");
2240
2241        assert_eq!(encoded, expected_encoded);
2242
2243        // Decode from the expected encoded string
2244        let decoded =
2245            PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
2246
2247        assert_eq!(decoded.unit, Some(CurrencyUnit::Custom("btc".to_string())));
2248        assert_eq!(decoded.payment_id, Some("custom_unit".to_string()));
2249    }
2250
2251    #[test]
2252    fn test_rejects_tlv_value_exceeding_u16_length() {
2253        let payment_request = PaymentRequest {
2254            payment_id: None,
2255            amount: None,
2256            unit: None,
2257            single_use: None,
2258            mints: vec![],
2259            description: Some("x".repeat(usize::from(u16::MAX) + 1)),
2260            transports: vec![],
2261            nut10: None,
2262        };
2263
2264        assert!(matches!(
2265            payment_request.to_bech32_string(),
2266            Err(Error::InvalidLength)
2267        ));
2268    }
2269
2270    #[test]
2271    fn test_decode_rejects_duplicate_nut10_tlv() {
2272        let nut10_a: [u8; 8] = [0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, b'A'];
2273        let nut10_b: [u8; 8] = [0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, b'B'];
2274
2275        let mut writer = TlvWriter::new();
2276        writer
2277            .write_tlv(0x08, &nut10_a)
2278            .expect("nut10_a should fit in TLV length");
2279        writer
2280            .write_tlv(0x08, &nut10_b)
2281            .expect("nut10_b should fit in TLV length");
2282
2283        let hrp = Hrp::parse(CREQ_B_HRP).expect("valid HRP");
2284        let encoded = bech32::encode_upper::<Bech32m>(hrp, &writer.into_bytes())
2285            .expect("valid bech32m encoding");
2286
2287        assert!(matches!(
2288            PaymentRequest::from_bech32_string(&encoded),
2289            Err(Error::InvalidStructure)
2290        ));
2291    }
2292
2293    #[test]
2294    fn test_rejects_tag_tuple_key_exceeding_u8_length() {
2295        let tag = vec!["x".repeat(usize::from(u8::MAX) + 1)];
2296
2297        assert!(matches!(
2298            PaymentRequest::encode_tag_tuple(&tag),
2299            Err(Error::TagTooLong)
2300        ));
2301    }
2302
2303    #[test]
2304    fn test_rejects_tag_tuple_value_exceeding_u8_length() {
2305        let tag = vec!["k".to_string(), "x".repeat(usize::from(u8::MAX) + 1)];
2306
2307        assert!(matches!(
2308            PaymentRequest::encode_tag_tuple(&tag),
2309            Err(Error::TagTooLong)
2310        ));
2311    }
2312}