1use 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
17pub const CREQ_B_HRP: &str = "creqb";
19
20#[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), },
51 }
52 }
53}
54
55struct 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
87struct 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
110impl PaymentRequest {
112 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 let encoded = bech32::encode_upper::<Bech32m>(hrp, &tlv_bytes)
164 .map_err(|_| Error::InvalidStructure)?;
165 Ok(encoded)
166 }
167
168 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 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 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 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 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 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 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 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 let transport = Self::decode_transport(&value)?;
290 transports.push(transport);
291 }
292 0x08 => {
293 if nut10.is_some() {
295 return Err(Error::InvalidStructure);
296 }
297 nut10 = Some(Self::decode_nut10(&value)?);
298 }
299 _ => {
300 }
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 fn encode_tlv(&self) -> Result<Vec<u8>, Error> {
319 let mut writer = TlvWriter::new();
320
321 if let Some(ref id) = self.payment_id {
323 writer.write_tlv(0x01, id.as_bytes())?;
324 }
325
326 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 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 if let Some(single_use) = self.single_use {
343 writer.write_tlv(0x04, &[if single_use { 1 } else { 0 }])?;
344 }
345
346 for mint in &self.mints {
348 writer.write_tlv(0x05, mint.to_string().as_bytes())?;
349 }
350
351 if let Some(ref description) = self.description {
353 writer.write_tlv(0x06, description.as_bytes())?;
354 }
355
356 for transport in &self.transports {
359 let transport_bytes = Self::encode_transport(transport)?;
360 writer.write_tlv(0x07, &transport_bytes)?;
361 }
362
363 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 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 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 match kind {
396 Some(0x00) => {
397 if value.len() != 32 {
399 return Err(Error::InvalidLength);
400 }
401 pubkey = Some(value);
402 }
403 Some(0x01) => {
404 http_target =
406 Some(String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?);
407 }
408 None => {
409 }
411 _ => return Err(Error::InvalidStructure),
412 }
413 }
414 0x03 => {
415 let tag_tuple = Self::decode_tag_tuple(&value)?;
417 tags.push(tag_tuple);
418 }
419 _ => {
420 }
422 }
423 }
424
425 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 let relays: Vec<String> = tags
435 .iter()
436 .filter(|(k, _)| k == "r")
437 .flat_map(|(_, v)| v.clone())
438 .collect();
439
440 let target = match transport_type {
442 TransportType::Nostr => {
443 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 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 fn encode_transport(transport: &Transport) -> Result<Vec<u8>, Error> {
473 let mut writer = TlvWriter::new();
474
475 let kind = match transport._type {
477 TransportType::Nostr => 0x00u8,
478 TransportType::HttpPost => 0x01u8,
479 };
480 writer.write_tlv(0x01, &[kind])?;
481
482 match transport._type {
484 TransportType::Nostr => {
485 let (pubkey, relays) = Self::decode_nprofile(&transport.target)?;
487
488 writer.write_tlv(0x02, &pubkey)?;
490
491 let mut all_relays = relays;
493
494 for tag in &transport.tags {
496 if tag.is_empty() {
497 continue;
498 }
499 if tag[0] == "n" && tag.len() >= 2 {
500 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 all_relays.push(tag[1].clone());
506 } else {
507 let tag_bytes = Self::encode_tag_tuple(tag)?;
509 writer.write_tlv(0x03, &tag_bytes)?;
510 }
511 }
512
513 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 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 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 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 if data.is_some() {
559 return Err(Error::InvalidStructure);
560 }
561 data = Some(value);
562 }
563 0x03 => {
564 let tag_tuple = Self::decode_tag_tuple(&value)?;
566 tags.push(tag_tuple);
567 }
568 _ => {
569 }
571 }
572 }
573
574 let kind_val = kind.ok_or(Error::InvalidStructure)?;
575 let data_val = data.unwrap_or_default();
576
577 let data_str = String::from_utf8(data_val).map_err(|_| Error::InvalidUtf8)?;
579
580 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 fn encode_nut10(nut10: &Nut10SecretRequest) -> Result<Vec<u8>, Error> {
608 let mut writer = TlvWriter::new();
609
610 let kind_val = match nut10.kind {
612 Kind::P2PK => 0u8,
613 Kind::HTLC => 1u8,
614 };
615 writer.write_tlv(0x01, &[kind_val])?;
616
617 writer.write_tlv(0x02, nut10.data.as_bytes())?;
619
620 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 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 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 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 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 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 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; }
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 if value.len() != 32 {
724 return Err(Error::InvalidLength);
725 }
726 pubkey = Some(value.to_vec());
727 }
728 1 => {
729 let relay =
731 String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidUtf8)?;
732 relays.push(relay);
733 }
734 _ => {
735 }
737 }
738 }
739
740 let pubkey = pubkey.ok_or(Error::InvalidStructure)?;
741 Ok((pubkey, relays))
742 }
743
744 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 tlv_bytes.push(0); tlv_bytes.push(32); tlv_bytes.extend_from_slice(pubkey);
757
758 for relay in relays {
760 if relay.len() > 255 {
761 return Err(Error::TagTooLong); }
763 tlv_bytes.push(1); tlv_bytes.push(relay.len() as u8); 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 assert!(encoded.starts_with("CREQB1"));
807
808 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 let legacy_creq = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
896
897 assert!(PaymentRequest::from_bech32_string(legacy_creq).is_err());
899
900 assert!(PaymentRequest::from_bech32_string("not_a_creq").is_err());
902
903 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 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 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 let pubkey_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
957 let pubkey_bytes = hex::decode(pubkey_hex).unwrap();
958
959 let nprofile =
961 PaymentRequest::encode_nprofile(&pubkey_bytes, &[]).expect("should encode nprofile");
962 assert!(nprofile.starts_with("nprofile"));
963
964 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 let nprofile = PaymentRequest::encode_nprofile(&pubkey_bytes, &relays)
983 .expect("should encode nprofile");
984 assert!(nprofile.starts_with("nprofile"));
985
986 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 let nostr_relays: Vec<RelayUrl> = relays
1007 .iter()
1008 .map(|r| RelayUrl::parse(r).expect("valid relay url"))
1009 .collect();
1010
1011 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 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 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 let nostr_relays: Vec<RelayUrl> = vec![];
1051
1052 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 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 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 let encoded = nostr_decoded.to_bech32().unwrap();
1085
1086 let re_decoded = Nip19Profile::from_bech32(&encoded)
1088 .expect("nostr-sdk should decode re-encoded nprofile");
1089
1090 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 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 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 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 assert!(decoded.transports[0].target.starts_with("nprofile"));
1176
1177 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 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 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 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 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 let pubkey1_hex = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
1243 let pubkey1_bytes = hex::decode(pubkey1_hex).unwrap();
1244 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 let encoded = payment_request
1289 .to_bech32_string()
1290 .expect("encoding should work");
1291
1292 println!("Encoded payment request: {}", encoded);
1293
1294 let decoded = PaymentRequest::from_bech32_string(&encoded)
1296 .expect("should decode valid bech32 string");
1297
1298 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 let mints = &decoded.mints;
1310 assert_eq!(mints.len(), 3);
1311
1312 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 assert_eq!(decoded.transports.len(), 2);
1330
1331 let transport1_decoded = &decoded.transports[0];
1333 assert_eq!(transport1_decoded._type, TransportType::Nostr);
1334 assert!(transport1_decoded.target.starts_with("nprofile"));
1335
1336 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 let tags1 = &transport1_decoded.tags;
1345 assert!(tags1
1346 .iter()
1347 .any(|t| t.len() >= 2 && t[0] == "n" && t[1] == "17"));
1348
1349 let transport2_decoded = &decoded.transports[1];
1351 assert_eq!(transport2_decoded._type, TransportType::Nostr);
1352 assert!(transport2_decoded.target.starts_with("nprofile"));
1353
1354 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 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]
1380 fn test_basic_payment_request() {
1381 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 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1400 let payment_request_cloned = payment_request.clone();
1401
1402 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 let encoded = payment_request
1424 .to_bech32_string()
1425 .expect("Failed to encode to bech32");
1426
1427 assert!(encoded.starts_with("CREQB1"));
1429
1430 assert_eq!(encoded, expected_encoded);
1432
1433 let decoded = PaymentRequest::from_bech32_string(&encoded).unwrap();
1435
1436 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 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 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 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 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 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1485 let payment_request_cloned = payment_request.clone();
1486
1487 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 let encoded = payment_request.to_bech32_string().unwrap();
1518
1519 assert_eq!(encoded, expected_encoded);
1521
1522 let decoded = PaymentRequest::from_str(&encoded).unwrap();
1523 assert_eq!(payment_request, decoded);
1524
1525 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 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 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1544 let payment_request_cloned = payment_request.clone();
1545
1546 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 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 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 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 let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
1589 let payment_request_cloned = payment_request.clone();
1590
1591 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 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 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 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 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 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 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 let expected_encoded = "CREQB1QYQQJ6R5W3C97AR9WD6QYQQGQQQQQQQQQQQ05QCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MG8QPQSZQQPQYPQQGNGW368QUE69UHKZURF9EJHSCTDWPKX2TNRDAKJ7A339ACXZ7TDV4H8GQCQZ5RXXATNW3HK6PNKV9K82EF3QEMXZMR4V5EQ9X3SJM";
1667
1668 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 assert_eq!(encoded, expected_encoded);
1677
1678 let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1680
1681 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 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 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 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 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 assert_eq!(encoded, expected_encoded);
1727
1728 let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1730
1731 let tags = &decoded.transports[0].tags;
1733
1734 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 assert_eq!(relay_values.len(), 3);
1748
1749 assert_eq!(
1751 "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gprpmhxue69uhhyetvv9unztn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5q3samnwvaz7tmjv4kxz7fn9ejhsctdwpkx2tnrdaksxzjpjp",
1752 decoded.transports[0].target
1753 );
1754
1755 let (_, decoded_relays) =
1757 PaymentRequest::decode_nprofile(&decoded.transports[0].target).unwrap();
1758 assert_eq!(decoded_relays.len(), 3);
1759
1760 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(encoded, expected_encoded);
1960
1961 let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
1963
1964 assert_eq!(decoded.transports.len(), 3);
1966
1967 assert_eq!(decoded.transports[0]._type, TransportType::Nostr);
1969 assert!(decoded.transports[0].target.starts_with("nprofile"));
1970
1971 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 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 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 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 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 assert_eq!(encoded, expected_encoded);
2023
2024 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 assert!(decoded.transports[0].tags.is_empty());
2033
2034 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 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 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 assert_eq!(encoded, expected_encoded);
2068
2069 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 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 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![], nut10: None,
2099 };
2100
2101 let encoded = payment_request
2102 .to_bech32_string()
2103 .expect("encoding should work");
2104
2105 let decoded = PaymentRequest::from_bech32_string(&encoded).expect("decoding should work");
2107
2108 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 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 let expected_encoded = "CREQB1QYQQJ6R5D3347AR9WD6QYQQGQQQQQQQQQQP7SQCQQYQQ2QQCDP68GURN8GHJ7MTFDE6ZUETCV9KHQMR99E3K7MGXQQF5S4ZVGVSXCMMRDDJKGGRSV9UK6ETWWSYQPTGPQQQSZQSQGFS46VR9XCMRSV3SVFNXYDP3XGERZVNRVCMKZC3NV3JKYVP5X5UKXEFJ8QEXZVTZXQ6XVERPXUMX2CFKXQERVCFKXAJNGVTPV5ERVE3NV33SXQQ5PPKX7CMTW35K6EG2XYMNQVPSXQCRQVPSQVQY5PNJV4N82MNYGGCRXVEJ8QCKXVEHXCMNWETPXGMNXETZXUCNSVMZXUURXVPKXANR2V35XSUNXVM9VCMNSEPCVVEKVVF4VGCKZDEHVD3RYDPKXQUNJCEJXEJS4EHJHC";
2135
2136 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 assert_eq!(encoded, expected_encoded);
2145
2146 let decoded =
2148 PaymentRequest::from_bech32_string(expected_encoded).expect("decoding should work");
2149
2150 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 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 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 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 let lowercase = uppercase.to_lowercase();
2206
2207 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 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 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 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}