1use alloc::string::{String, ToString};
7use alloc::vec;
8use alloc::vec::Vec;
9use core::fmt;
10use core::ops::Deref;
11use core::str::FromStr;
12
13use bitcoin::bech32::{self, Bech32m, Hrp};
14
15pub const CREQ_B_HRP: &str = "creqb";
17
18const INLINE_UNIT_BYTES: usize = 11;
21
22#[derive(Debug, PartialEq, Eq)]
24pub enum Error {
25 InvalidPrefix,
27 InvalidLength,
29 InvalidUtf8,
31 UnknownKind(u8),
33 Bech32,
35 InvalidStructure,
37}
38
39#[derive(Clone, PartialEq, Eq)]
44pub struct UnitString(UnitStringInner);
45
46#[derive(Clone, PartialEq, Eq)]
47enum UnitStringInner {
48 Inline { bytes: [u8; INLINE_UNIT_BYTES], len: u8 },
49 Heap(String),
50}
51
52impl UnitString {
53 pub fn new(s: &str) -> Self {
55 let bytes = s.as_bytes();
56 if bytes.len() <= INLINE_UNIT_BYTES {
57 let mut arr = [0u8; INLINE_UNIT_BYTES];
58 arr[..bytes.len()].copy_from_slice(bytes);
59 Self(UnitStringInner::Inline { bytes: arr, len: bytes.len() as u8 })
60 } else {
61 Self(UnitStringInner::Heap(s.to_string()))
62 }
63 }
64
65 pub fn from_utf8(bytes: &[u8]) -> Option<Self> {
67 core::str::from_utf8(bytes).ok().map(Self::new)
68 }
69
70 pub fn as_str(&self) -> &str {
72 match &self.0 {
73 UnitStringInner::Inline { bytes, len } => {
74 core::str::from_utf8(&bytes[..*len as usize])
77 .expect("UnitString contains valid UTF-8")
78 },
79 UnitStringInner::Heap(s) => s.as_str(),
80 }
81 }
82
83 pub fn as_bytes(&self) -> &[u8] {
85 self.as_str().as_bytes()
86 }
87}
88
89impl Deref for UnitString {
90 type Target = str;
91
92 fn deref(&self) -> &Self::Target {
93 self.as_str()
94 }
95}
96
97impl AsRef<str> for UnitString {
98 fn as_ref(&self) -> &str {
99 self.as_str()
100 }
101}
102
103impl fmt::Display for UnitString {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 fmt::Display::fmt(self.as_str(), f)
106 }
107}
108
109impl fmt::Debug for UnitString {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 fmt::Display::fmt(self.as_str(), f)
112 }
113}
114
115impl PartialEq<str> for UnitString {
116 fn eq(&self, other: &str) -> bool {
117 self.as_str() == other
118 }
119}
120
121impl PartialEq<&str> for UnitString {
122 fn eq(&self, other: &&str) -> bool {
123 self.as_str() == *other
124 }
125}
126
127impl From<&str> for UnitString {
128 fn from(s: &str) -> Self {
129 Self::new(s)
130 }
131}
132
133impl From<String> for UnitString {
134 fn from(s: String) -> Self {
135 if s.len() <= INLINE_UNIT_BYTES {
136 Self::new(&s)
137 } else {
138 Self(UnitStringInner::Heap(s))
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum CurrencyUnit {
149 Sat,
151 Msat,
153 Usd,
156 Eur,
159 Auth,
170 Custom(UnitString),
173}
174
175impl CurrencyUnit {
176 pub fn custom(s: &str) -> Self {
178 Self::Custom(UnitString::new(s))
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum TransportType {
189 Nostr,
191 HttpPost,
193}
194
195#[derive(Copy, Clone, Eq)]
198pub struct TagValue {
199 bytes: [u8; 255],
200 len: u8,
201}
202
203impl TagValue {
204 pub fn new(s: &str) -> Result<Self, Error> {
206 let bytes = s.as_bytes();
207 if bytes.len() > u8::MAX as usize {
208 return Err(Error::InvalidLength);
209 }
210 let mut arr = [0u8; 255];
211 arr[..bytes.len()].copy_from_slice(bytes);
212 Ok(Self { bytes: arr, len: bytes.len() as u8 })
213 }
214
215 pub fn as_str(&self) -> &str {
217 core::str::from_utf8(&self.bytes[..self.len as usize])
219 .expect("TagValue contains valid UTF-8")
220 }
221
222 pub fn as_bytes(&self) -> &[u8] {
224 self.as_str().as_bytes()
225 }
226}
227
228impl Deref for TagValue {
229 type Target = str;
230
231 fn deref(&self) -> &Self::Target {
232 self.as_str()
233 }
234}
235
236impl AsRef<str> for TagValue {
237 fn as_ref(&self) -> &str {
238 self.as_str()
239 }
240}
241
242impl fmt::Display for TagValue {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 f.write_str(self.as_str())
245 }
246}
247
248impl fmt::Debug for TagValue {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.write_str(self.as_str())
251 }
252}
253
254impl PartialEq for TagValue {
255 fn eq(&self, other: &Self) -> bool {
256 self.as_str() == other.as_str()
257 }
258}
259
260impl PartialEq<str> for TagValue {
261 fn eq(&self, other: &str) -> bool {
262 self.as_str() == other
263 }
264}
265
266impl PartialEq<&str> for TagValue {
267 fn eq(&self, other: &&str) -> bool {
268 self.as_str() == *other
269 }
270}
271
272impl PartialEq<String> for TagValue {
273 fn eq(&self, other: &String) -> bool {
274 self.as_str() == other.as_str()
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct TagTuple {
285 key: TagValue,
287 values: Vec<TagValue>,
289}
290
291impl TagTuple {
292 pub fn new<I, S>(key: &str, values: I) -> Result<Self, Error>
296 where
297 I: IntoIterator<Item = S>,
298 S: AsRef<str>,
299 {
300 let key = TagValue::new(key)?;
301 let values_iter = values.into_iter();
302 let mut tag_values = Vec::with_capacity(values_iter.size_hint().0);
303 for value in values_iter {
304 tag_values.push(TagValue::new(value.as_ref())?);
305 }
306 Ok(Self { key, values: tag_values })
307 }
308
309 pub fn single(key: &str, value: &str) -> Result<Self, Error> {
313 let key = TagValue::new(key)?;
314 Ok(Self { key, values: vec![TagValue::new(value)?] })
315 }
316
317 pub fn key(&self) -> &str {
319 &self.key
320 }
321
322 pub fn values(&self) -> &[TagValue] {
324 &self.values
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct Transport {
339 pub kind: TransportType,
341 pub target: String,
343 pub tags: Vec<TagTuple>,
351}
352
353impl Transport {
354 pub fn nostr_relays(&self) -> Result<Vec<String>, Error> {
358 if self.kind == TransportType::Nostr {
359 Ok(CashuPaymentRequest::decode_nprofile(&self.target)?.1)
360 } else {
361 Err(Error::InvalidStructure)
362 }
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum Kind {
372 P2PK,
377 HTLC,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct Nut10SecretRequest {
391 pub kind: Kind,
393 pub data: String,
398 pub tags: Vec<TagTuple>,
405}
406
407impl Nut10SecretRequest {
408 pub fn new(kind: Kind, data: &str, tags: Vec<TagTuple>) -> Self {
410 Self { kind, data: data.to_string(), tags }
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct CashuPaymentRequest {
422 pub payment_id: Option<String>,
424 pub amount: Option<u64>,
426 pub unit: Option<CurrencyUnit>,
429 pub single_use: Option<bool>,
431 pub mints: Option<Vec<String>>,
433 pub description: Option<String>,
435 pub transports: Vec<Transport>,
438 pub nut10: Option<Nut10SecretRequest>,
440}
441
442struct TlvReader<'a> {
444 data: &'a [u8],
445 position: usize,
446}
447
448impl<'a> TlvReader<'a> {
449 fn new(data: &'a [u8]) -> Self {
450 Self { data, position: 0 }
451 }
452
453 fn read_tlv(&mut self) -> Result<Option<(u8, &'a [u8])>, Error> {
454 if self.position + 3 > self.data.len() {
455 return Ok(None);
456 }
457
458 let tag = self.data[self.position];
459 let len = u16::from_be_bytes([self.data[self.position + 1], self.data[self.position + 2]])
460 as usize;
461 self.position += 3;
462
463 if self.position + len > self.data.len() {
464 return Err(Error::InvalidLength);
465 }
466
467 let value = &self.data[self.position..self.position + len];
468 self.position += len;
469
470 Ok(Some((tag, value)))
471 }
472}
473
474struct TlvWriter {
480 data: Vec<u8>,
481}
482
483impl TlvWriter {
484 fn with_capacity(capacity: usize) -> Self {
485 Self { data: Vec::with_capacity(capacity) }
486 }
487
488 fn write_tlv(&mut self, tag: u8, value: &[u8]) {
489 self.data.push(tag);
490 let len = value.len() as u16;
491 self.data.extend_from_slice(&len.to_be_bytes());
492 self.data.extend_from_slice(value);
493 }
494
495 fn write_raw(&mut self, bytes: &[u8]) {
497 self.data.extend_from_slice(bytes);
498 }
499
500 fn write_byte(&mut self, byte: u8) {
502 self.data.push(byte);
503 }
504
505 fn into_bytes(self) -> Vec<u8> {
506 self.data
507 }
508}
509
510struct SingleTlvWriter<'a> {
513 writer: &'a mut TlvWriter,
514 len_pos: usize,
515}
516
517impl<'a> SingleTlvWriter<'a> {
518 fn new(writer: &'a mut TlvWriter, tag: u8) -> Self {
519 writer.data.push(tag);
520 let len_pos = writer.data.len();
521 writer.data.extend_from_slice(&[0, 0]); Self { writer, len_pos }
523 }
524
525 fn nested(&mut self, tag: u8) -> SingleTlvWriter<'_> {
527 SingleTlvWriter::new(self.writer, tag)
528 }
529
530 fn write_tlv(&mut self, tag: u8, value: &[u8]) {
531 self.writer.write_tlv(tag, value);
532 }
533
534 fn write_raw(&mut self, bytes: &[u8]) {
535 self.writer.write_raw(bytes);
536 }
537
538 fn write_byte(&mut self, byte: u8) {
539 self.writer.write_byte(byte);
540 }
541}
542
543impl Drop for SingleTlvWriter<'_> {
544 fn drop(&mut self) {
545 let value_len = self.writer.data.len() - (self.len_pos + 2);
546 let len_bytes = (value_len as u16).to_be_bytes();
547 self.writer.data[self.len_pos] = len_bytes[0];
548 self.writer.data[self.len_pos + 1] = len_bytes[1];
549 }
550}
551
552impl FromStr for CashuPaymentRequest {
553 type Err = Error;
554
555 fn from_str(s: &str) -> Result<Self, Self::Err> {
556 Self::from_bech32_string(s)
557 }
558}
559
560impl fmt::Display for CashuPaymentRequest {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 let s = self.to_bech32_string().map_err(|_| fmt::Error)?;
563 f.write_str(&s)
564 }
565}
566
567impl CashuPaymentRequest {
568 pub fn to_bech32_string(&self) -> Result<String, Error> {
590 let tlv_bytes = self.encode_tlv()?;
591 let hrp = Hrp::parse(CREQ_B_HRP).map_err(|_| Error::InvalidPrefix)?;
592
593 let encoded =
595 bech32::encode_upper::<Bech32m>(hrp, &tlv_bytes).map_err(|_| Error::Bech32)?;
596 Ok(encoded)
597 }
598
599 pub fn from_bech32_string(s: &str) -> Result<Self, Error> {
601 let (hrp, data) = bech32::decode(s).map_err(|_| Error::Bech32)?;
604 if !hrp.as_str().eq_ignore_ascii_case(CREQ_B_HRP) {
605 return Err(Error::InvalidPrefix);
606 }
607
608 Self::from_bech32_bytes(&data)
609 }
610
611 #[cfg(fuzzing)]
612 pub fn from_bytes_fuzzy(bytes: &[u8]) -> Result<CashuPaymentRequest, Error> {
614 Self::from_bech32_bytes(bytes)
615 }
616
617 fn from_bech32_bytes(bytes: &[u8]) -> Result<CashuPaymentRequest, Error> {
619 let mut reader = TlvReader::new(bytes);
620
621 let mut id: Option<String> = None;
622 let mut amount: Option<u64> = None;
623 let mut unit: Option<CurrencyUnit> = None;
624 let mut single_use: Option<bool> = None;
625 let mut mints: Vec<String> = Vec::new();
626 let mut description: Option<String> = None;
627 let mut transports: Vec<Transport> = Vec::new();
628 let mut nut10: Option<Nut10SecretRequest> = None;
629
630 while let Some((tag, value)) = reader.read_tlv()? {
631 match tag {
632 0x01 => {
633 if id.is_some() {
635 return Err(Error::InvalidStructure);
636 }
637 id = Some(String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?);
638 },
639 0x02 => {
640 if amount.is_some() {
642 return Err(Error::InvalidStructure);
643 }
644 if value.len() != 8 {
645 return Err(Error::InvalidLength);
646 }
647 let amount_val = u64::from_be_bytes([
648 value[0], value[1], value[2], value[3], value[4], value[5], value[6],
649 value[7],
650 ]);
651 amount = Some(amount_val);
652 },
653 0x03 => {
654 if unit.is_some() {
656 return Err(Error::InvalidStructure);
657 }
658 if value.len() == 1 && value[0] == 0 {
659 unit = Some(CurrencyUnit::Sat);
660 } else {
661 match value {
662 b"msat" => unit = Some(CurrencyUnit::Msat),
663 b"usd" => unit = Some(CurrencyUnit::Usd),
664 b"eur" => unit = Some(CurrencyUnit::Eur),
665 b"auth" => unit = Some(CurrencyUnit::Auth),
666 _ => {
667 let unit_str =
668 UnitString::from_utf8(value).ok_or(Error::InvalidUtf8)?;
669 unit = Some(CurrencyUnit::Custom(unit_str));
670 },
671 }
672 }
673 },
674 0x04 => {
675 if single_use.is_some() {
677 return Err(Error::InvalidStructure);
678 }
679 if !value.is_empty() {
680 single_use = Some(value[0] != 0);
681 }
682 },
683 0x05 => {
684 let mint_str =
686 String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?;
687 mints.push(mint_str);
688 },
689 0x06 => {
690 if description.is_some() {
692 return Err(Error::InvalidStructure);
693 }
694 description =
695 Some(String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?);
696 },
697 0x07 => {
698 let transport = Self::decode_transport(value)?;
700 transports.push(transport);
701 },
702 0x08 => {
703 if nut10.is_some() {
705 return Err(Error::InvalidStructure);
706 }
707 nut10 = Some(Self::decode_nut10(value)?);
708 },
709 _ => {
710 },
712 }
713 }
714
715 Ok(CashuPaymentRequest {
716 payment_id: id,
717 amount,
718 unit,
719 single_use,
720 mints: if mints.is_empty() { None } else { Some(mints) },
721 description,
722 transports,
723 nut10,
724 })
725 }
726
727 fn encode_tlv(&self) -> Result<Vec<u8>, Error> {
729 let estimated_capacity = 64
740 + self.payment_id.as_ref().map_or(0, |s| s.len() + 3)
741 + self.amount.map_or(0, |_| 11)
742 + self.unit.as_ref().map_or(0, |_| 10)
743 + self.mints.as_ref().map_or(0, |m| m.iter().map(|s| s.len() + 3).sum())
744 + self.description.as_ref().map_or(0, |s| s.len() + 3)
745 + self.transports.len() * 150
746 + self.nut10.as_ref().map_or(0, |_| 100);
747
748 let mut writer = TlvWriter::with_capacity(estimated_capacity);
749
750 if let Some(ref id) = self.payment_id {
752 writer.write_tlv(0x01, id.as_bytes());
753 }
754
755 if let Some(amount) = self.amount {
757 let amount_bytes = amount.to_be_bytes();
758 writer.write_tlv(0x02, &amount_bytes);
759 }
760
761 if let Some(ref unit) = self.unit {
763 match unit {
764 CurrencyUnit::Sat => writer.write_tlv(0x03, &[0]),
765 CurrencyUnit::Msat => writer.write_tlv(0x03, b"msat"),
766 CurrencyUnit::Usd => writer.write_tlv(0x03, b"usd"),
767 CurrencyUnit::Eur => writer.write_tlv(0x03, b"eur"),
768 CurrencyUnit::Auth => writer.write_tlv(0x03, b"auth"),
769 CurrencyUnit::Custom(s) => writer.write_tlv(0x03, s.as_bytes()),
770 }
771 }
772
773 if let Some(single_use) = self.single_use {
775 writer.write_tlv(0x04, &[if single_use { 1 } else { 0 }]);
776 }
777
778 if let Some(ref mints) = self.mints {
780 for mint in mints {
781 writer.write_tlv(0x05, mint.as_bytes());
782 }
783 }
784
785 if let Some(ref description) = self.description {
787 writer.write_tlv(0x06, description.as_bytes());
788 }
789
790 for transport in &self.transports {
792 let mut w = SingleTlvWriter::new(&mut writer, 0x07);
793 Self::encode_transport_into(transport, &mut w)?;
794 }
795
796 if let Some(ref nut10) = self.nut10 {
798 let mut w = SingleTlvWriter::new(&mut writer, 0x08);
799 Self::encode_nut10_into(nut10, &mut w)?;
800 }
801
802 Ok(writer.into_bytes())
803 }
804
805 fn decode_transport(bytes: &[u8]) -> Result<Transport, Error> {
807 let mut reader = TlvReader::new(bytes);
808
809 let mut kind: Option<u8> = None;
810 let mut raw_target: Option<&[u8]> = None;
811 let mut tags: Vec<(&str, Vec<&str>)> = Vec::new();
812
813 while let Some((tag, value)) = reader.read_tlv()? {
814 match tag {
815 0x01 => {
816 if kind.is_some() {
818 return Err(Error::InvalidStructure);
819 }
820 if value.len() != 1 {
821 return Err(Error::InvalidLength);
822 }
823 kind = Some(value[0]);
824 },
825 0x02 => {
826 if raw_target.is_some() {
828 return Err(Error::InvalidStructure);
829 }
830 raw_target = Some(value);
831 },
832 0x03 => {
833 let tag_tuple = Self::decode_tag_tuple(value)?;
835 tags.push(tag_tuple);
836 },
837 _ => {
838 },
840 }
841 }
842
843 let transport_type = match kind.ok_or(Error::InvalidStructure)? {
844 0x00 => TransportType::Nostr,
845 0x01 => TransportType::HttpPost,
846 _ => return Err(Error::InvalidStructure),
847 };
848
849 let relays: Vec<&str> =
850 tags.iter().filter(|(k, _)| *k == "r").flat_map(|(_, v)| v.iter().copied()).collect();
851
852 let raw_target = raw_target.ok_or(Error::InvalidStructure)?;
854 let target = match transport_type {
855 TransportType::Nostr => {
856 if raw_target.len() != 32 {
858 return Err(Error::InvalidLength);
859 }
860 Self::encode_nprofile(raw_target, &relays)?
861 },
862 TransportType::HttpPost => {
863 String::from_utf8(raw_target.to_vec()).map_err(|_| Error::InvalidUtf8)?
865 },
866 };
867
868 let mut final_tags: Vec<TagTuple> = Vec::new();
869 for (key, values) in tags {
870 if key != "r" {
871 final_tags.push(TagTuple::new(key, values)?);
872 }
873 }
874
875 Ok(Transport { kind: transport_type, target, tags: final_tags })
876 }
877
878 fn encode_transport_into(
880 transport: &Transport, writer: &mut SingleTlvWriter<'_>,
881 ) -> Result<(), Error> {
882 let kind = match transport.kind {
883 TransportType::Nostr => 0x00u8,
884 TransportType::HttpPost => 0x01u8,
885 };
886 writer.write_tlv(0x01, &[kind]);
887
888 match transport.kind {
889 TransportType::Nostr => {
890 let (pubkey, relays) = Self::decode_nprofile(&transport.target)?;
891
892 writer.write_tlv(0x02, &pubkey);
893
894 for tag in transport.tags.iter() {
895 Self::encode_tag_tuple_into(tag, writer);
896 }
897
898 for relay in relays {
899 Self::encode_tag_tuple_into(&TagTuple::single("r", &relay)?, writer);
900 }
901 },
902 TransportType::HttpPost => {
903 writer.write_tlv(0x02, transport.target.as_bytes());
904
905 for tag in transport.tags.iter() {
906 Self::encode_tag_tuple_into(tag, writer);
907 }
908 },
909 }
910
911 Ok(())
912 }
913
914 fn decode_nut10(bytes: &[u8]) -> Result<Nut10SecretRequest, Error> {
916 let mut reader = TlvReader::new(bytes);
917
918 let mut kind: Option<u8> = None;
919 let mut data: Option<Vec<u8>> = None;
920 let mut tags: Vec<TagTuple> = Vec::new();
921
922 while let Some((tag, value)) = reader.read_tlv()? {
923 match tag {
924 0x01 => {
925 if kind.is_some() {
927 return Err(Error::InvalidStructure);
928 }
929 if value.len() != 1 {
930 return Err(Error::InvalidLength);
931 }
932 kind = Some(value[0]);
933 },
934 0x02 => {
935 if data.is_some() {
937 return Err(Error::InvalidStructure);
938 }
939 data = Some(value.to_vec());
940 },
941 0x03 | 0x05 => {
942 let (key, values) = Self::decode_tag_tuple(value)?;
944 tags.push(TagTuple::new(key, values)?);
945 },
946 _ => {
947 },
949 }
950 }
951
952 let kind_val = kind.ok_or(Error::InvalidStructure)?;
953 let data_val = data.unwrap_or_default();
954
955 let data_str = String::from_utf8(data_val).map_err(|_| Error::InvalidUtf8)?;
956
957 let kind_enum = match kind_val {
958 0 => Kind::P2PK,
959 1 => Kind::HTLC,
960 _ => return Err(Error::UnknownKind(kind_val)),
961 };
962
963 Ok(Nut10SecretRequest::new(kind_enum, &data_str, tags))
964 }
965
966 fn encode_nut10_into(
968 nut10: &Nut10SecretRequest, writer: &mut SingleTlvWriter<'_>,
969 ) -> Result<(), Error> {
970 let kind_val = match nut10.kind {
971 Kind::P2PK => 0u8,
972 Kind::HTLC => 1u8,
973 };
974 writer.write_tlv(0x01, &[kind_val]);
975 writer.write_tlv(0x02, nut10.data.as_bytes());
976
977 for tag in nut10.tags.iter() {
978 Self::encode_tag_tuple_into(tag, writer);
979 }
980
981 Ok(())
982 }
983
984 fn decode_tag_tuple(bytes: &[u8]) -> Result<(&str, Vec<&str>), Error> {
986 if bytes.is_empty() {
987 return Err(Error::InvalidLength);
988 }
989
990 let key_len = bytes[0] as usize;
991 if bytes.len() < 1 + key_len {
992 return Err(Error::InvalidLength);
993 }
994
995 let key = core::str::from_utf8(&bytes[1..1 + key_len]).map_err(|_| Error::InvalidUtf8)?;
996
997 let mut values = Vec::new();
998 let mut pos = 1 + key_len;
999
1000 while pos < bytes.len() {
1001 let val_len = bytes[pos] as usize;
1002 pos += 1;
1003
1004 if pos + val_len > bytes.len() {
1005 return Err(Error::InvalidLength);
1006 }
1007
1008 let value =
1009 core::str::from_utf8(&bytes[pos..pos + val_len]).map_err(|_| Error::InvalidUtf8)?;
1010 values.push(value);
1011 pos += val_len;
1012 }
1013
1014 Ok((key, values))
1015 }
1016
1017 fn encode_tag_tuple_into(tag: &TagTuple, writer: &mut SingleTlvWriter<'_>) {
1020 let mut w = writer.nested(0x03);
1021
1022 w.write_byte(tag.key().len() as u8);
1024 w.write_raw(tag.key().as_bytes());
1025
1026 for value in tag.values() {
1028 w.write_byte(value.len() as u8);
1029 w.write_raw(value.as_bytes());
1030 }
1031 }
1032
1033 fn decode_nprofile(nprofile: &str) -> Result<([u8; 32], Vec<String>), Error> {
1035 let (hrp, data) = bech32::decode(nprofile).map_err(|_| Error::Bech32)?;
1036 if hrp.as_str() != "nprofile" {
1037 return Err(Error::InvalidPrefix);
1038 }
1039
1040 let mut pos = 0;
1041 let mut pubkey: Option<[u8; 32]> = None;
1042 let mut relays: Vec<String> = Vec::new();
1043
1044 while pos < data.len() {
1045 if pos + 2 > data.len() {
1046 break;
1047 }
1048
1049 let tag = data[pos];
1050 let len = data[pos + 1] as usize;
1051 pos += 2;
1052
1053 if pos + len > data.len() {
1054 return Err(Error::InvalidLength);
1055 }
1056
1057 let value = &data[pos..pos + len];
1058 pos += len;
1059
1060 match tag {
1061 0 => {
1062 if value.len() != 32 {
1064 return Err(Error::InvalidLength);
1065 }
1066 pubkey = Some(value.try_into().expect("len is 32"));
1067 },
1068 1 => {
1069 let relay =
1071 String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?;
1072 relays.push(relay);
1073 },
1074 _ => {
1075 },
1077 }
1078 }
1079
1080 let pubkey = pubkey.ok_or(Error::InvalidStructure)?;
1081 Ok((pubkey, relays))
1082 }
1083
1084 fn encode_nprofile(pubkey: &[u8], relays: &[&str]) -> Result<String, Error> {
1086 if pubkey.len() != 32 {
1087 return Err(Error::InvalidLength);
1088 }
1089
1090 let capacity = 34 + relays.iter().map(|r| 2 + r.len()).sum::<usize>();
1091 let mut tlv_bytes = Vec::with_capacity(capacity);
1092
1093 tlv_bytes.push(0);
1095 tlv_bytes.push(32);
1096 tlv_bytes.extend_from_slice(pubkey);
1097
1098 for relay in relays {
1100 if relay.len() > 255 {
1101 return Err(Error::InvalidLength);
1102 }
1103 tlv_bytes.push(1);
1104 tlv_bytes.push(relay.len() as u8);
1105 tlv_bytes.extend_from_slice(relay.as_bytes());
1106 }
1107
1108 let hrp = Hrp::parse("nprofile").map_err(|_| Error::InvalidPrefix)?;
1109 bech32::encode::<bech32::Bech32>(hrp, &tlv_bytes).map_err(|_| Error::Bech32)
1110 }
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use alloc::string::ToString;
1117 use bitcoin::hex::FromHex;
1118
1119 #[test]
1120 fn test_bech32_basic_round_trip() {
1121 let transport = Transport {
1122 kind: TransportType::HttpPost,
1123 target: "https://api.example.com/payment".to_string(),
1124 tags: Vec::new(),
1125 };
1126
1127 let payment_request = CashuPaymentRequest {
1128 payment_id: Some("test123".to_string()),
1129 amount: Some(100),
1130 unit: Some(CurrencyUnit::Sat),
1131 single_use: Some(true),
1132 mints: Some(vec!["https://mint.example.com".to_string()]),
1133 description: Some("Test payment".to_string()),
1134 transports: vec![transport],
1135 nut10: None,
1136 };
1137
1138 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1139
1140 assert!(encoded.starts_with("CREQB1"));
1142
1143 let decoded =
1145 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1146 assert_eq!(decoded.payment_id, payment_request.payment_id);
1147 assert_eq!(decoded.amount, payment_request.amount);
1148 assert_eq!(decoded.unit, payment_request.unit);
1149 assert_eq!(decoded.single_use, payment_request.single_use);
1150 assert_eq!(decoded.description, payment_request.description);
1151 }
1152
1153 #[test]
1154 fn test_bech32_minimal() {
1155 let payment_request = CashuPaymentRequest {
1156 payment_id: Some("minimal".to_string()),
1157 amount: None,
1158 unit: Some(CurrencyUnit::Sat),
1159 single_use: None,
1160 mints: Some(vec!["https://mint.example.com".to_string()]),
1161 description: None,
1162 transports: vec![],
1163 nut10: None,
1164 };
1165
1166 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1167
1168 let decoded =
1169 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1170 assert_eq!(decoded.payment_id, payment_request.payment_id);
1171 assert_eq!(decoded.mints, payment_request.mints);
1172 }
1173
1174 #[test]
1175 fn test_bech32_with_nut10() {
1176 let nut10 = Nut10SecretRequest::new(
1177 Kind::P2PK,
1178 "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
1179 vec![TagTuple::single("timeout", "3600").unwrap()],
1180 );
1181
1182 let payment_request = CashuPaymentRequest {
1183 payment_id: Some("nut10test".to_string()),
1184 amount: Some(500),
1185 unit: Some(CurrencyUnit::Sat),
1186 single_use: None,
1187 mints: Some(vec!["https://mint.example.com".to_string()]),
1188 description: Some("P2PK locked payment".to_string()),
1189 transports: vec![],
1190 nut10: Some(nut10.clone()),
1191 };
1192
1193 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1194
1195 let decoded =
1196 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1197 assert_eq!(decoded.nut10.as_ref().unwrap().kind, nut10.kind);
1198 assert_eq!(decoded.nut10.as_ref().unwrap().data, nut10.data);
1199 }
1200
1201 #[test]
1202 fn test_parse_creq_param_bech32() {
1203 let payment_request = CashuPaymentRequest {
1204 payment_id: Some("test123".to_string()),
1205 amount: Some(100),
1206 unit: Some(CurrencyUnit::Sat),
1207 single_use: None,
1208 mints: Some(vec!["https://mint.example.com".to_string()]),
1209 description: None,
1210 transports: vec![],
1211 nut10: None,
1212 };
1213
1214 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1215
1216 let decoded_payment_request =
1217 CashuPaymentRequest::from_bech32_string(&encoded).expect("should parse bech32");
1218 assert_eq!(decoded_payment_request.payment_id, payment_request.payment_id);
1219 }
1220
1221 #[test]
1222 fn test_from_bech32_string_errors_on_wrong_encoding() {
1223 let legacy_creq = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
1225
1226 assert!(CashuPaymentRequest::from_bech32_string(legacy_creq).is_err());
1228
1229 assert!(CashuPaymentRequest::from_bech32_string("not_a_creq").is_err());
1231
1232 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1234 let pubkey_bytes = Vec::<u8>::from_hex(pubkey_hex).unwrap();
1235 let nprofile = CashuPaymentRequest::encode_nprofile(&pubkey_bytes, &[])
1236 .expect("should encode nprofile");
1237 assert!(CashuPaymentRequest::from_bech32_string(&nprofile).is_err());
1238 }
1239
1240 #[test]
1241 fn test_unit_encoding_bech32() {
1242 let payment_request = CashuPaymentRequest {
1244 payment_id: Some("unit_test".to_string()),
1245 amount: Some(100),
1246 unit: Some(CurrencyUnit::Sat),
1247 single_use: None,
1248 mints: Some(vec!["https://mint.example.com".to_string()]),
1249 description: None,
1250 transports: vec![],
1251 nut10: None,
1252 };
1253
1254 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1255
1256 let decoded =
1257 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1258 assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1259
1260 let payment_request_usd = CashuPaymentRequest {
1262 payment_id: Some("unit_test_usd".to_string()),
1263 amount: Some(100),
1264 unit: Some(CurrencyUnit::Usd),
1265 single_use: None,
1266 mints: Some(vec!["https://mint.example.com".to_string()]),
1267 description: None,
1268 transports: vec![],
1269 nut10: None,
1270 };
1271
1272 let encoded_usd = payment_request_usd.to_bech32_string().expect("encoding should work");
1273
1274 let decoded_usd =
1275 CashuPaymentRequest::from_bech32_string(&encoded_usd).expect("decoding should work");
1276 assert_eq!(decoded_usd.unit, Some(CurrencyUnit::Usd));
1277 }
1278
1279 #[test]
1280 fn test_nprofile_no_relays() {
1281 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1283 let pubkey_bytes = Vec::<u8>::from_hex(pubkey_hex).unwrap();
1284
1285 let nprofile = CashuPaymentRequest::encode_nprofile(&pubkey_bytes, &[])
1287 .expect("should encode nprofile");
1288 assert!(nprofile.starts_with("nprofile"));
1289
1290 let decoded =
1292 CashuPaymentRequest::decode_nprofile(&nprofile).expect("should decode nprofile");
1293 assert_eq!(&decoded.0[..], &pubkey_bytes[..]);
1294 assert!(decoded.1.is_empty());
1295 }
1296
1297 #[test]
1298 fn test_nostr_transport_with_nprofile_no_relays() {
1299 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1301 let pubkey_bytes = Vec::<u8>::from_hex(pubkey_hex).unwrap();
1302 let nprofile =
1303 CashuPaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("encode nprofile");
1304
1305 let transport = Transport {
1306 kind: TransportType::Nostr,
1307 target: nprofile.clone(),
1308 tags: vec![TagTuple::single("n", "17").unwrap()],
1309 };
1310
1311 let payment_request = CashuPaymentRequest {
1312 payment_id: Some("nostr_test".to_string()),
1313 amount: Some(1000),
1314 unit: Some(CurrencyUnit::Sat),
1315 single_use: None,
1316 mints: Some(vec!["https://mint.example.com".to_string()]),
1317 description: Some("Nostr payment".to_string()),
1318 transports: vec![transport],
1319 nut10: None,
1320 };
1321
1322 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1323
1324 let decoded =
1325 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1326
1327 assert_eq!(decoded.payment_id, payment_request.payment_id);
1328 assert_eq!(decoded.transports.len(), 1);
1329 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1330 assert!(decoded.transports[0].target.starts_with("nprofile"));
1331
1332 let mut tags_iter = decoded.transports[0].tags.iter();
1334 assert!(
1335 tags_iter.any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("17"))
1336 );
1337 }
1338
1339 #[test]
1340 fn test_nostr_transport_with_nprofile() {
1341 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1343 let pubkey_bytes = Vec::<u8>::from_hex(pubkey_hex).unwrap();
1344 let relays: Vec<&str> = vec!["wss://relay.example.com"];
1345 let nprofile =
1346 CashuPaymentRequest::encode_nprofile(&pubkey_bytes, &relays).expect("encode nprofile");
1347
1348 let transport = Transport {
1349 kind: TransportType::Nostr,
1350 target: nprofile.clone(),
1351 tags: vec![TagTuple::single("n", "17").unwrap()],
1352 };
1353
1354 let payment_request = CashuPaymentRequest {
1355 payment_id: Some("nprofile_test".to_string()),
1356 amount: Some(2100),
1357 unit: Some(CurrencyUnit::Sat),
1358 single_use: None,
1359 mints: Some(vec!["https://mint.example.com".to_string()]),
1360 description: Some("Nostr payment with relays".to_string()),
1361 transports: vec![transport],
1362 nut10: None,
1363 };
1364
1365 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1366
1367 let decoded =
1368 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1369
1370 assert_eq!(decoded.payment_id, payment_request.payment_id);
1371 assert_eq!(decoded.transports.len(), 1);
1372 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1373
1374 assert!(decoded.transports[0].target.starts_with("nprofile"));
1376
1377 let relays = decoded.transports[0].nostr_relays().unwrap();
1379 assert_eq!(relays[0].as_str(), "wss://relay.example.com");
1380 }
1381
1382 #[test]
1383 fn test_spec_example_nostr_transport() {
1384 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1387 let pubkey_bytes = Vec::<u8>::from_hex(pubkey_hex).unwrap();
1388 let relays: Vec<&str> = vec!["wss://relay.damus.io"];
1389 let nprofile =
1390 CashuPaymentRequest::encode_nprofile(&pubkey_bytes, &relays).expect("encode nprofile");
1391
1392 let transport = Transport {
1393 kind: TransportType::Nostr,
1394 target: nprofile,
1395 tags: vec![TagTuple::single("n", "17").unwrap()],
1396 };
1397
1398 let payment_request = CashuPaymentRequest {
1399 payment_id: Some("spec_example".to_string()),
1400 amount: Some(10),
1401 unit: Some(CurrencyUnit::Sat),
1402 single_use: Some(true),
1403 mints: Some(vec!["https://mint.example.com".to_string()]),
1404 description: Some("Coffee".to_string()),
1405 transports: vec![transport],
1406 nut10: None,
1407 };
1408
1409 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1411
1412 let decoded =
1413 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1414
1415 assert_eq!(decoded.payment_id, Some("spec_example".to_string()));
1417 assert_eq!(decoded.amount, Some(10));
1418 assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1419 assert_eq!(decoded.single_use, Some(true));
1420 assert_eq!(decoded.description, Some("Coffee".to_string()));
1421 assert_eq!(decoded.transports.len(), 1);
1422 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1423
1424 let mut tags_iter = decoded.transports[0].tags.iter();
1426 assert!(
1427 tags_iter.any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("17"))
1428 );
1429
1430 let nostr_relays = decoded.transports[0].nostr_relays().unwrap();
1431 assert_eq!(nostr_relays[0].as_str(), "wss://relay.damus.io");
1432 }
1433
1434 #[test]
1435 fn test_decode_valid_bech32_with_nostr_pubkeys_and_mints() {
1436 let pubkey1_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1438 let pubkey1_bytes = Vec::<u8>::from_hex(pubkey1_hex).unwrap();
1439 let nprofile1 =
1441 CashuPaymentRequest::encode_nprofile(&pubkey1_bytes, &[]).expect("encode nprofile1");
1442
1443 let pubkey2_hex = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
1444 let pubkey2_bytes = Vec::<u8>::from_hex(pubkey2_hex).unwrap();
1445 let relays2: Vec<&str> = vec!["wss://relay.damus.io", "wss://nos.lol"];
1446 let nprofile2 = CashuPaymentRequest::encode_nprofile(&pubkey2_bytes, &relays2)
1447 .expect("encode nprofile2");
1448
1449 let transport1 = Transport {
1450 kind: TransportType::Nostr,
1451 target: nprofile1.clone(),
1452 tags: vec![TagTuple::single("n", "17").unwrap()],
1453 };
1454
1455 let transport2 = Transport {
1456 kind: TransportType::Nostr,
1457 target: nprofile2.clone(),
1458 tags: vec![TagTuple::single("n", "17").unwrap(), TagTuple::single("n", "44").unwrap()],
1459 };
1460
1461 let payment_request = CashuPaymentRequest {
1462 payment_id: Some("multi_test".to_string()),
1463 amount: Some(5000),
1464 unit: Some(CurrencyUnit::Sat),
1465 single_use: Some(false),
1466 mints: Some(vec![
1467 "https://mint1.example.com".to_string(),
1468 "https://mint2.example.com".to_string(),
1469 "https://testnut.cashu.space".to_string(),
1470 ]),
1471 description: Some("Payment with multiple transports and mints".to_string()),
1472 transports: vec![transport1, transport2],
1473 nut10: None,
1474 };
1475
1476 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1478
1479 let decoded = CashuPaymentRequest::from_bech32_string(&encoded)
1481 .expect("should decode valid bech32 string");
1482
1483 assert_eq!(decoded.payment_id, Some("multi_test".to_string()));
1485 assert_eq!(decoded.amount, Some(5000));
1486 assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1487 assert_eq!(decoded.single_use, Some(false));
1488 assert_eq!(
1489 decoded.description,
1490 Some("Payment with multiple transports and mints".to_string())
1491 );
1492
1493 let mints = decoded.mints.as_ref().expect("should have mints");
1495 assert_eq!(mints.len(), 3);
1496
1497 assert_eq!(decoded.transports.len(), 2);
1499
1500 let transport1_decoded = &decoded.transports[0];
1502 assert_eq!(transport1_decoded.kind, TransportType::Nostr);
1503 assert!(transport1_decoded.target.starts_with("nprofile"));
1504
1505 let (decoded_pubkey1, decoded_relays1) =
1507 CashuPaymentRequest::decode_nprofile(&transport1_decoded.target)
1508 .expect("should decode nprofile");
1509 assert_eq!(&decoded_pubkey1[..], &pubkey1_bytes[..]);
1510 assert!(decoded_relays1.is_empty());
1511
1512 let mut tags1_iter = transport1_decoded.tags.iter();
1514 assert!(
1515 tags1_iter.any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("17"))
1516 );
1517
1518 let transport2_decoded = &decoded.transports[1];
1520 assert_eq!(transport2_decoded.kind, TransportType::Nostr);
1521 assert!(transport2_decoded.target.starts_with("nprofile"));
1522
1523 let (decoded_pubkey2, decoded_relays2) =
1525 CashuPaymentRequest::decode_nprofile(&transport2_decoded.target)
1526 .expect("should decode nprofile");
1527 assert_eq!(&decoded_pubkey2[..], &pubkey2_bytes[..]);
1528 assert_eq!(decoded_relays2, relays2);
1529
1530 let tags2 = &transport2_decoded.tags;
1532 assert!(tags2
1533 .iter()
1534 .any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("17")));
1535 assert!(tags2
1536 .iter()
1537 .any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("44")));
1538 let relays = transport2_decoded.nostr_relays().unwrap();
1539 assert!(relays.iter().any(|v| v == "wss://relay.damus.io"));
1540 assert!(relays.iter().any(|v| v == "wss://nos.lol"));
1541 }
1542
1543 #[test]
1544 fn test_basic_payment_request() {
1545 let expected_encoded = "CREQB1QYQQSC3HVYUNQVFHXCPQQZQQQQQQQQQQQQ9QXQQPQQZSQ9MGW368QUE69UHNSVENXVH8XURPVDJN5VENXVUQWQREQYQQZQQZQQSGM6QFA3C8DTZ2FVZHVFQEACMWM0E50PE3K5TFMVPJJMN0VJ7M2TGRQQZSZMSZXYMSXQQHQ9EPGAMNWVAZ7TMJV4KXZ7FWV3SK6ATN9E5K7QCQRGQHY9MHWDEN5TE0WFJKCCTE9CURXVEN9EEHQCTRV5HSXQQSQ9EQ6AMNWVAZ7TMWDAEJUMR0DSRYDPGF";
1562
1563 let transport = Transport {
1565 kind: TransportType::Nostr,
1566 target: "nprofile1qqsgm6qfa3c8dtz2fvzhvfqeacmwm0e50pe3k5tfmvpjjmn0vj7m2tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3wamnwvaz7tmjv4kxz7fw8qenxvewwdcxzcm99uqs6amnwvaz7tmwdaejumr0ds4ljh7n".to_string(),
1567 tags: vec![TagTuple::single("n", "17").unwrap()],
1568 };
1569
1570 let payment_request = CashuPaymentRequest {
1571 payment_id: Some("b7a90176".to_string()),
1572 amount: Some(10),
1573 unit: Some(CurrencyUnit::Sat),
1574 single_use: None,
1575 mints: Some(vec!["https://8333.space:3338".to_string()]),
1576 description: None,
1577 transports: vec![transport],
1578 nut10: None,
1579 };
1580
1581 let encoded = payment_request.to_bech32_string().expect("Failed to encode to bech32");
1583
1584 assert_eq!(encoded, expected_encoded);
1586
1587 let decoded = CashuPaymentRequest::from_bech32_string(&encoded).unwrap();
1589
1590 assert_eq!(decoded.payment_id.as_ref().unwrap(), "b7a90176");
1592 assert_eq!(decoded.amount.unwrap(), 10);
1593 assert_eq!(decoded.unit.unwrap(), CurrencyUnit::Sat);
1594 assert_eq!(decoded.mints.unwrap(), vec!["https://8333.space:3338".to_string()]);
1595
1596 assert_eq!(decoded.transports.len(), 1);
1598 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1599 let mut tags_iter = decoded.transports[0].tags.iter();
1600 assert!(
1601 tags_iter.any(|t| t.key == "n" && t.values.first().map(|s| s.as_str()) == Some("17"))
1602 );
1603 }
1604
1605 #[test]
1606 fn test_nostr_transport_payment_request() {
1607 let expected_encoded = "CREQB1QYQQSE3EXFSN2VTZ8QPQQZQQQQQQQQQQQPJQXQQPQQZSQXTGW368QUE69UHK66TWWSCJUETCV9KHQMR99E3K7MG9QQVKSAR5WPEN5TE0D45KUAPJ9EJHSCTDWPKX2TNRDAKSWQPEQYQQZQQZQQSQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQRQQZSZMSZXYMSXQQ8Q9HQGWFHXV6SCAGZ48";
1608
1609 let transport = Transport {
1610 kind: TransportType::Nostr,
1611 target: "nprofile1qqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8uzqt"
1612 .to_string(),
1613 tags: vec![
1614 TagTuple::single("n", "17").unwrap(),
1615 TagTuple::single("n", "9735").unwrap(),
1616 ],
1617 };
1618
1619 let payment_request = CashuPaymentRequest {
1620 payment_id: Some("f92a51b8".to_string()),
1621 amount: Some(100),
1622 unit: Some(CurrencyUnit::Sat),
1623 single_use: None,
1624 mints: Some(vec![
1625 "https://mint1.example.com".to_string(),
1626 "https://mint2.example.com".to_string(),
1627 ]),
1628 description: None,
1629 transports: vec![transport],
1630 nut10: None,
1631 };
1632
1633 let encoded = payment_request.to_bech32_string().unwrap();
1635
1636 assert_eq!(encoded, expected_encoded);
1638
1639 let decoded = CashuPaymentRequest::from_bech32_string(&encoded).unwrap();
1640 assert_eq!(payment_request, decoded);
1641 }
1642
1643 #[test]
1644 fn test_minimal_payment_request_vectors() {
1645 let expected_encoded =
1646 "CREQB1QYQQSDMXX3SNYC3N8YPSQQGQQ5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKSYP0LHG";
1647
1648 let payment_request = CashuPaymentRequest {
1649 payment_id: Some("7f4a2b39".to_string()),
1650 amount: None,
1651 unit: Some(CurrencyUnit::Sat),
1652 single_use: None,
1653 mints: Some(vec!["https://mint.example.com".to_string()]),
1654 description: None,
1655 transports: vec![],
1656 nut10: None,
1657 };
1658
1659 let encoded = payment_request.to_bech32_string().unwrap();
1661 assert_eq!(encoded, expected_encoded);
1662 let decoded = CashuPaymentRequest::from_bech32_string(&encoded).unwrap();
1663 assert_eq!(payment_request, decoded);
1664 }
1665
1666 #[test]
1667 fn test_nut10_locking_payment_request_vectors() {
1668 let expected_encoded = "CREQB1QYQQSCEEV56R2EPJVYPQQZQQQQQQQQQQQ86QXQQPQQZSQXRGW368QUE69UHK66TWWSHX27RPD4CXCEFWVDHK6ZQQTYQSQQGQQGQYYVPJVVEKYDTZVGERWEFNXCCNGDFHVVUNYEPEXDJRWWRYVSMNXEPNVS6NXDENXGCNZVRZXF3KVEFCVG6NQENZVVCXZCNRXCCN2EFEVVENXVGRQQXSWARFD4JK7AT5QSENVVPS2N5FAS";
1669
1670 let nut10 = Nut10SecretRequest {
1671 kind: Kind::P2PK,
1672 data: "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331".to_string(),
1673 tags: vec![TagTuple::single("timeout", "3600").unwrap()],
1674 };
1675
1676 let payment_request = CashuPaymentRequest {
1677 payment_id: Some("c9e45d2a".to_string()),
1678 amount: Some(500),
1679 unit: Some(CurrencyUnit::Sat),
1680 single_use: None,
1681 mints: Some(vec!["https://mint.example.com".to_string()]),
1682 description: None,
1683 transports: vec![],
1684 nut10: Some(nut10),
1685 };
1686
1687 let encoded = payment_request.to_bech32_string().unwrap();
1689 assert_eq!(encoded, expected_encoded);
1690 let decoded = CashuPaymentRequest::from_bech32_string(&encoded).unwrap();
1691 assert_eq!(payment_request, decoded);
1692 }
1693
1694 #[test]
1695 fn test_nut26_example() {
1696 let expected_encoded = "CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ";
1697
1698 let payment_request = CashuPaymentRequest {
1699 payment_id: Some("demo123".to_string()),
1700 amount: Some(1000),
1701 unit: Some(CurrencyUnit::Sat),
1702 single_use: Some(true),
1703 mints: Some(vec!["https://mint.example.com".to_string()]),
1704 description: Some("Coffee payment".to_string()),
1705 transports: vec![],
1706 nut10: None,
1707 };
1708
1709 let encoded = payment_request.to_bech32_string().unwrap();
1710
1711 assert_eq!(expected_encoded, encoded);
1712 }
1713
1714 #[test]
1715 fn test_http_post_transport_kind_1() {
1716 let expected_encoded = "CREQB1QYQQJ6R5W3C97AR9WD6QYQQGQQQQQQQQQQQ05QCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MG8QPQSZQQPQYPQQGNGW368QUE69UHKZURF9EJHSCTDWPKX2TNRDAKJ7A339ACXZ7TDV4H8GQCQZ5RXXATNW3HK6PNKV9K82EF3QEMXZMR4V5EQ9X3SJM";
1717
1718 let transport = Transport {
1719 kind: TransportType::HttpPost,
1720 target: "https://api.example.com/v1/payment".to_string(),
1721 tags: vec![
1722 TagTuple::new("custom", vec!["value1".to_string(), "value2".to_string()]).unwrap()
1723 ],
1724 };
1725
1726 let payment_request = CashuPaymentRequest {
1727 payment_id: Some("http_test".to_string()),
1728 amount: Some(250),
1729 unit: Some(CurrencyUnit::Sat),
1730 single_use: None,
1731 mints: Some(vec!["https://mint.example.com".to_string()]),
1732 description: None,
1733 transports: vec![transport],
1734 nut10: None,
1735 };
1736
1737 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1738
1739 assert_eq!(encoded, expected_encoded);
1741
1742 let decoded =
1744 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1745
1746 assert_eq!(decoded.transports.len(), 1);
1748 assert_eq!(decoded.transports[0].kind, TransportType::HttpPost);
1749 assert_eq!(decoded.transports[0].target, "https://api.example.com/v1/payment");
1750
1751 let mut tags_iter = decoded.transports[0].tags.iter();
1753 assert!(tags_iter.any(|t| t.key == "custom"
1754 && t.values.len() >= 2
1755 && t.values[0] == "value1"
1756 && t.values[1] == "value2"));
1757 }
1758
1759 #[test]
1760 fn test_relay_tag_extraction_from_nprofile() {
1761 let expected_encoded = "CREQB1QYQQ5UN9D3SHJHM5V4EHGQSQPQQQQQQQQQQQQEQRQQQSQPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDQUQGZQGQQYQQYQPQ80CVV07TJDRRGPA0J7J7TMNYL2YR6YR7L8J4S3EVF6U64TH6GKWSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7F39EJHSCTDWPKX2TNRDAKSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7FJ9EJHSCTDWPKX2TNRDAKSXQQMQ9EPSAMNWVAZ7TMJV4KXZ7FN9EJHSCTDWPKX2TNRDAKSKRFDAR";
1762
1763 let transport = Transport {
1764 kind: TransportType::Nostr,
1765 target: "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gprpmhxue69uhhyetvv9unztn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5q3samnwvaz7tmjv4kxz7fn9ejhsctdwpkx2tnrdaksxzjpjp".to_string(),
1766 tags: Vec::new(),
1767 };
1768
1769 let payment_request = CashuPaymentRequest {
1770 payment_id: Some("relay_test".to_string()),
1771 amount: Some(100),
1772 unit: Some(CurrencyUnit::Sat),
1773 single_use: None,
1774 mints: Some(vec!["https://mint.example.com".to_string()]),
1775 description: None,
1776 transports: vec![transport],
1777 nut10: None,
1778 };
1779
1780 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1781
1782 assert_eq!(encoded, expected_encoded);
1784
1785 let decoded =
1787 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1788
1789 let nostr_relays = decoded.transports[0].nostr_relays().unwrap();
1791 assert_eq!(nostr_relays.len(), 3);
1792
1793 assert_eq!(
1795 "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gprpmhxue69uhhyetvv9unztn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5q3samnwvaz7tmjv4kxz7fn9ejhsctdwpkx2tnrdaksxzjpjp",
1796 decoded.transports[0].target
1797 );
1798 }
1799
1800 #[test]
1801 fn test_multiple_transports() {
1802 let expected_encoded = "CREQB1QYQQ7MT4D36XJHM5WFSKUUMSDAE8GQSQPQQQQQQQQQQQRAQRQQQSQPGQRP58GARSWVAZ7TMDD9H8GTN90PSK6URVV5HXXMMDQCQZQ5RP09KK2MN5YPMKJARGYPKH2MR5D9CXCEFQW3EXZMNNWPHHYARNQUQZ7QGQQYQQYQPQ80CVV07TJDRRGPA0J7J7TMNYL2YR6YR7L8J4S3EVF6U64TH6GKWSXQQ9Q9HQYVFHQUQZWQGQQYQSYQPQDP68GURN8GHJ7CTSDYCJUETCV9KHQMR99E3K7MF0WPSHJMT9DE6QWQP6QYQQZQGZQQSXSAR5WPEN5TE0V9CXJV3WV4UXZMTSD3JJUCM0D5HHQCTED4JKUAQRQQGQSURJD9HHY6T50YRXYCTRDD6HQTSH7TP";
1803
1804 let t1 = Transport {
1805 kind: TransportType::Nostr,
1806 target: "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8g2lcy6q"
1807 .to_string(),
1808 tags: vec![TagTuple::single("n", "17").unwrap()],
1809 };
1810 let t2 = Transport {
1811 kind: TransportType::HttpPost,
1812 target: "https://api1.example.com/payment".to_string(),
1813 tags: Vec::new(),
1814 };
1815 let t3 = Transport {
1816 kind: TransportType::HttpPost,
1817 target: "https://api2.example.com/payment".to_string(),
1818 tags: vec![TagTuple::single("priority", "backup").unwrap()],
1819 };
1820
1821 let payment_request = CashuPaymentRequest {
1822 payment_id: Some("multi_transport".to_string()),
1823 amount: Some(500),
1824 unit: Some(CurrencyUnit::Sat),
1825 mints: Some(vec!["https://mint.example.com".to_string()]),
1826 description: Some("Payment with multiple transports".to_string()),
1827 single_use: None,
1828 transports: vec![t1, t2, t3],
1829 nut10: None,
1830 };
1831
1832 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1833
1834 assert_eq!(encoded, expected_encoded);
1836
1837 let decoded =
1839 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1840
1841 assert_eq!(decoded.transports.len(), 3);
1843
1844 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1846 assert!(decoded.transports[0].target.starts_with("nprofile"));
1847
1848 assert_eq!(decoded.transports[1].kind, TransportType::HttpPost);
1850 assert_eq!(decoded.transports[1].target, "https://api1.example.com/payment");
1851
1852 assert_eq!(decoded.transports[2].kind, TransportType::HttpPost);
1854 assert_eq!(decoded.transports[2].target, "https://api2.example.com/payment");
1855 let mut tags_iter = decoded.transports[2].tags.iter();
1856 assert!(tags_iter
1857 .any(|t| t.key == "priority" && t.values.first().map(|s| s.as_str()) == Some("backup")));
1858 }
1859
1860 #[test]
1861 fn test_minimal_transport_nostr_only_pubkey() {
1862 let expected_encoded = "CREQB1QYQQ6MTFDE5K6CTVTAHX7UM5WGPSQQGQQ5QPS6R5W3C8XW309AKKJMN59EJHSCTDWPKX2TNRDAKSWQP8QYQQZQQZQQSRHUXX8L9EX335Q7HE0F09AEJ04ZPAZPL0NE2CGUKYAWD24MAYT8G7QNXMQ";
1863
1864 let transport = Transport {
1865 kind: TransportType::Nostr,
1866 target: "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8g2lcy6q"
1867 .to_string(),
1868 tags: Vec::new(),
1869 };
1870
1871 let payment_request = CashuPaymentRequest {
1872 payment_id: Some("minimal_nostr".to_string()),
1873 unit: Some(CurrencyUnit::Sat),
1874 mints: Some(vec!["https://mint.example.com".to_string()]),
1875 amount: None,
1876 description: None,
1877 single_use: None,
1878 transports: vec![transport],
1879 nut10: None,
1880 };
1881
1882 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1883
1884 assert_eq!(encoded, expected_encoded);
1886
1887 let decoded =
1889 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1890
1891 assert_eq!(decoded.transports.len(), 1);
1892 assert_eq!(decoded.transports[0].kind, TransportType::Nostr);
1893 assert!(decoded.transports[0].target.starts_with("nprofile"));
1894
1895 assert!(decoded.transports[0].tags.is_empty());
1897 }
1898
1899 #[test]
1900 fn test_minimal_transport_http_just_url() {
1901 let expected_encoded = "CREQB1QYQQCMTFDE5K6CTVTA58GARSQVQQZQQ9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RSQ8SPQQQSZQSQZA58GARSWVAZ7TMPWP5JUETCV9KHQMR99E3K7MG0TWYGX";
1902
1903 let transport = Transport {
1904 kind: TransportType::HttpPost,
1905 target: "https://api.example.com".to_string(),
1906 tags: Vec::new(),
1907 };
1908
1909 let payment_request = CashuPaymentRequest {
1910 payment_id: Some("minimal_http".to_string()),
1911 unit: Some(CurrencyUnit::Sat),
1912 mints: Some(vec!["https://mint.example.com".to_string()]),
1913 amount: None,
1914 description: None,
1915 single_use: None,
1916 transports: vec![transport],
1917 nut10: None,
1918 };
1919
1920 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1921
1922 assert_eq!(encoded, expected_encoded);
1924
1925 let decoded =
1927 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1928
1929 assert_eq!(decoded.transports.len(), 1);
1930 assert_eq!(decoded.transports[0].kind, TransportType::HttpPost);
1931 assert_eq!(decoded.transports[0].target, "https://api.example.com");
1932 assert!(decoded.transports[0].tags.is_empty());
1933 }
1934
1935 #[test]
1936 fn test_nut10_htlc_kind_1() {
1937 let expected_encoded = "CREQB1QYQQJ6R5D3347AR9WD6QYQQGQQQQQQQQQQP7SQCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MGXQQF5S4ZVGVSXCMMRDDJKGGRSV9UK6ETWWSYQPTGPQQQSZQSQGFS46VR9XCMRSV3SVFNXYDP3XGERZVNRVCMKZC3NV3JKYVP5X5UKXEFJ8QEXZVTZXQ6XVERPXUMX2CFKXQERVCFKXAJNGVTPV5ERVE3NV33SXQQ5PPKX7CMTW35K6EG2XYMNQVPSXQCRQVPSQVQY5PNJV4N82MNYGGCRXVEJ8QCKXVEHXCMNWETPXGMNXETZXUCNSVMZXUURXVPKXANR2V35XSUNXVM9VCMNSEPCVVEKVVF4VGCKZDEHVD3RYDPKXQUNJCEJXEJS4EHJHC";
1938
1939 let nut10 = Nut10SecretRequest {
1940 kind: Kind::HTLC,
1941 data: "a]0e66820bfb412212cf7ab3deb0459ce282a1b04fda76ea6026a67e41ae26f3dc".to_string(),
1942 tags: vec![
1943 TagTuple::single("locktime", "1700000000").unwrap(),
1944 TagTuple::single(
1945 "refund",
1946 "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
1947 )
1948 .unwrap(),
1949 ],
1950 };
1951
1952 let payment_request = CashuPaymentRequest {
1953 payment_id: Some("htlc_test".to_string()),
1954 amount: Some(1000),
1955 unit: Some(CurrencyUnit::Sat),
1956 mints: Some(vec!["https://mint.example.com".to_string()]),
1957 description: Some("HTLC locked payment".to_string()),
1958 single_use: None,
1959 transports: vec![],
1960 nut10: Some(nut10),
1961 };
1962
1963 let encoded = payment_request.to_bech32_string().expect("encoding should work");
1964
1965 assert_eq!(encoded, expected_encoded);
1967
1968 let decoded = CashuPaymentRequest::from_bech32_string(&expected_encoded)
1970 .expect("decoding should work");
1971
1972 assert_eq!(decoded.payment_id, Some("htlc_test".to_string()));
1974 assert_eq!(decoded.amount, Some(1000));
1975 assert_eq!(decoded.unit, Some(CurrencyUnit::Sat));
1976 assert_eq!(decoded.mints, Some(vec!["https://mint.example.com".to_string()]));
1977 assert_eq!(decoded.description, Some("HTLC locked payment".to_string()));
1978
1979 let nut10 = decoded.nut10.as_ref().unwrap();
1981 assert_eq!(nut10.kind, Kind::HTLC);
1982 assert_eq!(
1983 nut10.data,
1984 "a]0e66820bfb412212cf7ab3deb0459ce282a1b04fda76ea6026a67e41ae26f3dc"
1985 );
1986
1987 let tags = &nut10.tags;
1989 assert_eq!(tags.len(), 2);
1990 assert_eq!(tags[0], TagTuple::single("locktime", "1700000000").unwrap());
1991 assert_eq!(
1992 tags[1],
1993 TagTuple::single(
1994 "refund",
1995 "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e"
1996 )
1997 .unwrap()
1998 );
1999 }
2000
2001 #[test]
2002 fn test_case_insensitive_decoding() {
2003 let payment_request = CashuPaymentRequest {
2004 payment_id: Some("case_test".to_string()),
2005 amount: Some(100),
2006 unit: Some(CurrencyUnit::Sat),
2007 single_use: None,
2008 mints: Some(vec!["https://mint.example.com".to_string()]),
2009 description: None,
2010 transports: vec![],
2011 nut10: None,
2012 };
2013
2014 let uppercase = payment_request.to_bech32_string().expect("encoding should work");
2015
2016 let lowercase = uppercase.to_lowercase();
2018
2019 let decoded_upper =
2021 CashuPaymentRequest::from_bech32_string(&uppercase).expect("uppercase should decode");
2022 let decoded_lower =
2023 CashuPaymentRequest::from_bech32_string(&lowercase).expect("lowercase should decode");
2024
2025 assert_eq!(decoded_upper.payment_id, Some("case_test".to_string()));
2027 assert_eq!(decoded_lower.payment_id, Some("case_test".to_string()));
2028
2029 assert_eq!(decoded_upper.amount, decoded_lower.amount);
2030 assert_eq!(decoded_upper.unit, decoded_lower.unit);
2031 }
2032
2033 #[test]
2034 fn test_custom_currency_unit() {
2035 let expected_encoded = "CREQB1QYQQKCM4WD6X7M2LW4HXJAQZQQYQQQQQQQQQQQRYQVQQXCN5VVZSQXRGW368QUE69UHK66TWWSHX27RPD4CXCEFWVDHK6PZHCW8";
2036
2037 let payment_request = CashuPaymentRequest {
2038 payment_id: Some("custom_unit".to_string()),
2039 amount: Some(100),
2040 unit: Some(CurrencyUnit::custom("btc")),
2041 single_use: None,
2042 mints: Some(vec!["https://mint.example.com".to_string()]),
2043 description: None,
2044 transports: vec![],
2045 nut10: None,
2046 };
2047
2048 let encoded = payment_request.to_bech32_string().expect("encoding should work");
2049
2050 assert_eq!(encoded, expected_encoded);
2051
2052 let decoded =
2054 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2055
2056 assert_eq!(decoded.unit, Some(CurrencyUnit::custom("btc")));
2057 assert_eq!(decoded.payment_id, Some("custom_unit".to_string()));
2058 }
2059
2060 #[test]
2061 fn test_custom_currency_unit_long() {
2062 let long_unit = "this_is_a_very_long_unit_name";
2064
2065 let payment_request = CashuPaymentRequest {
2066 payment_id: None,
2067 amount: Some(100),
2068 unit: Some(CurrencyUnit::custom(long_unit)),
2069 single_use: None,
2070 mints: None,
2071 description: None,
2072 transports: vec![],
2073 nut10: None,
2074 };
2075
2076 let encoded = payment_request.to_bech32_string().expect("encoding should work");
2077 let decoded =
2078 CashuPaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2079
2080 assert_eq!(decoded.unit, Some(CurrencyUnit::custom(long_unit)));
2081 assert_eq!(decoded.amount, Some(100));
2082 }
2083
2084 #[test]
2085 fn test_transport_tlv_ordering_target_before_kind() {
2086 let pubkey = [0x42u8; 32]; let mut writer = TlvWriter::with_capacity(64);
2092 writer.write_tlv(0x02, &pubkey); writer.write_tlv(0x01, &[0x00]); let bytes = writer.into_bytes();
2095
2096 let transport = CashuPaymentRequest::decode_transport(&bytes)
2097 .expect("should decode transport with target before kind");
2098 assert_eq!(transport.kind, TransportType::Nostr);
2099 assert!(transport.target.starts_with("nprofile"));
2101
2102 let url = b"https://example.com/callback";
2104 let mut writer = TlvWriter::with_capacity(64);
2105 writer.write_tlv(0x02, url); writer.write_tlv(0x01, &[0x01]); let bytes = writer.into_bytes();
2108
2109 let transport = CashuPaymentRequest::decode_transport(&bytes)
2110 .expect("should decode HTTP transport with target before kind");
2111 assert_eq!(transport.kind, TransportType::HttpPost);
2112 assert_eq!(transport.target, "https://example.com/callback");
2113 }
2114
2115 #[test]
2116 fn test_duplicate_tlv_fields() {
2117 let mut writer = TlvWriter::with_capacity(32);
2119 writer.write_tlv(0x02, &100u64.to_be_bytes());
2120 writer.write_tlv(0x02, &200u64.to_be_bytes());
2121 let bytes = writer.into_bytes();
2122 assert_eq!(CashuPaymentRequest::from_bech32_bytes(&bytes), Err(Error::InvalidStructure));
2123
2124 let mut writer = TlvWriter::with_capacity(16);
2126 writer.write_tlv(0x01, &[0x00]); writer.write_tlv(0x01, &[0x01]); let bytes = writer.into_bytes();
2129 assert_eq!(CashuPaymentRequest::decode_transport(&bytes), Err(Error::InvalidStructure));
2130
2131 let pubkey = vec![0u8; 32];
2133 let mut writer = TlvWriter::with_capacity(128);
2134 writer.write_tlv(0x01, &[0x00]); writer.write_tlv(0x02, &pubkey);
2136 writer.write_tlv(0x02, &pubkey);
2137 let bytes = writer.into_bytes();
2138 assert_eq!(CashuPaymentRequest::decode_transport(&bytes), Err(Error::InvalidStructure));
2139
2140 let mut writer = TlvWriter::with_capacity(64);
2142 writer.write_tlv(0x01, &[0x01]); writer.write_tlv(0x02, b"https://example.com");
2144 writer.write_tlv(0x02, b"https://example.org");
2145 let bytes = writer.into_bytes();
2146 assert_eq!(CashuPaymentRequest::decode_transport(&bytes), Err(Error::InvalidStructure));
2147
2148 let mut writer = TlvWriter::with_capacity(16);
2150 writer.write_tlv(0x01, &[0x00]); writer.write_tlv(0x01, &[0x01]); let bytes = writer.into_bytes();
2153 assert_eq!(CashuPaymentRequest::decode_nut10(&bytes), Err(Error::InvalidStructure));
2154
2155 let mut writer = TlvWriter::with_capacity(32);
2157 writer.write_tlv(0x01, &[0x00]); writer.write_tlv(0x02, b"data1");
2159 writer.write_tlv(0x02, b"data2");
2160 let bytes = writer.into_bytes();
2161 assert_eq!(CashuPaymentRequest::decode_nut10(&bytes), Err(Error::InvalidStructure));
2162 }
2163}