1#![deny(missing_docs)]
31#![forbid(unsafe_code)]
32#![deny(rustdoc::broken_intra_doc_links)]
33#![deny(rustdoc::private_intra_doc_links)]
34#![cfg_attr(not(feature = "std"), no_std)]
35
36extern crate alloc;
37extern crate core;
38
39use alloc::borrow::ToOwned;
40use alloc::str::FromStr;
41use alloc::string::String;
42use alloc::vec;
43use alloc::vec::Vec;
44
45use bitcoin::{address, Address, Network};
46use core::time::Duration;
47use lightning::offers::offer::{self, Offer};
48use lightning::offers::parse::Bolt12ParseError;
49use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef, ParseOrSemanticError};
50
51#[cfg(feature = "std")]
52mod dnssec_utils;
53
54#[cfg(feature = "std")]
55pub mod dns_resolver;
56
57#[cfg(feature = "http")]
58pub mod http_resolver;
59
60pub mod amount;
61
62pub mod receive;
63
64pub mod hrn_resolution;
65
66pub mod hrn;
67
68use amount::Amount;
69use hrn::HumanReadableName;
70use hrn_resolution::{HrnResolution, HrnResolver};
71
72#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum PaymentMethod {
75 LightningBolt11(Bolt11Invoice),
77 LightningBolt12(Offer),
79 OnChain(Address),
81}
82
83impl PaymentMethod {
84 fn amount(&self) -> Option<Amount> {
85 match self {
86 PaymentMethod::LightningBolt11(invoice) => {
87 invoice.amount_milli_satoshis().map(|amt_msat| {
88 let res = Amount::from_milli_sats(amt_msat);
89 debug_assert!(res.is_ok(), "This should be rejected at parse-time");
90 res.unwrap_or(Amount::ZERO)
91 })
92 },
93 PaymentMethod::LightningBolt12(offer) => match offer.amount() {
94 Some(offer::Amount::Bitcoin { amount_msats }) => {
95 let res = Amount::from_milli_sats(amount_msats);
96 debug_assert!(res.is_ok(), "This should be rejected at parse-time");
97 Some(res.unwrap_or(Amount::ZERO))
98 },
99 Some(offer::Amount::Currency { .. }) => None,
100 None => None,
101 },
102 PaymentMethod::OnChain(_) => None,
103 }
104 }
105
106 fn is_lightning(&self) -> bool {
107 match self {
108 PaymentMethod::LightningBolt11(_) => true,
109 PaymentMethod::LightningBolt12(_) => true,
110 PaymentMethod::OnChain(_) => false,
111 }
112 }
113
114 fn has_fixed_amount(&self) -> bool {
115 match self {
116 PaymentMethod::LightningBolt11(invoice) => invoice.amount_milli_satoshis().is_some(),
117 PaymentMethod::LightningBolt12(offer) => match offer.amount() {
118 Some(offer::Amount::Bitcoin { .. }) => true,
119 Some(offer::Amount::Currency { .. }) => true,
120 None => false,
121 },
122 PaymentMethod::OnChain(_) => false,
123 }
124 }
125}
126
127pub enum PossiblyResolvedPaymentMethod<'a> {
129 LNURLPay {
132 min_value: Amount,
134 max_value: Amount,
136 callback: &'a str,
139 },
140 Resolved(&'a PaymentMethod),
142}
143
144pub enum PaymentMethodType {
150 LightningBolt11,
153 LightningBolt12,
156 OnChain,
159}
160
161impl<'a> PossiblyResolvedPaymentMethod<'a> {
162 pub fn method_type(&self) -> PaymentMethodType {
164 match self {
165 Self::LNURLPay { .. } => PaymentMethodType::LightningBolt11,
166 Self::Resolved(PaymentMethod::LightningBolt11(_)) => PaymentMethodType::LightningBolt11,
167 Self::Resolved(PaymentMethod::LightningBolt12(_)) => PaymentMethodType::LightningBolt12,
168 Self::Resolved(PaymentMethod::OnChain(_)) => PaymentMethodType::OnChain,
169 }
170 }
171}
172
173#[derive(Clone, PartialEq, Eq, Debug)]
174struct PaymentInstructionsImpl {
175 description: Option<String>,
176 methods: Vec<PaymentMethod>,
177 ln_amt: Option<Amount>,
178 onchain_amt: Option<Amount>,
179 lnurl: Option<(String, [u8; 32], Amount, Amount)>,
180 pop_callback: Option<String>,
181 hrn: Option<HumanReadableName>,
182 hrn_proof: Option<Vec<u8>>,
183}
184
185macro_rules! common_methods {
187 ($struct: ty) => {
188 impl $struct {
189 #[inline]
196 pub fn recipient_description(&self) -> Option<&str> {
197 self.inner().description.as_ref().map(|d| d.as_str())
198 }
199
200 #[inline]
207 pub fn pop_callback(&self) -> Option<&str> {
208 self.inner().pop_callback.as_ref().map(|c| c.as_str())
209 }
210
211 #[inline]
214 pub fn human_readable_name(&self) -> &Option<HumanReadableName> {
215 &self.inner().hrn
216 }
217
218 #[inline]
227 pub fn bip_353_dnssec_proof(&self) -> &Option<Vec<u8>> {
228 &self.inner().hrn_proof
229 }
230 }
231 };
232}
233
234#[derive(Clone, PartialEq, Eq, Debug)]
237pub struct FixedAmountPaymentInstructions {
238 inner: PaymentInstructionsImpl,
239}
240
241impl FixedAmountPaymentInstructions {
242 pub fn max_amount(&self) -> Option<Amount> {
251 core::cmp::max(self.inner.ln_amt, self.inner.onchain_amt)
252 }
253
254 pub fn ln_payment_amount(&self) -> Option<Amount> {
269 self.inner.ln_amt
270 }
271
272 pub fn onchain_payment_amount(&self) -> Option<Amount> {
279 self.inner.onchain_amt
280 }
281
282 #[inline]
284 pub fn methods(&self) -> &[PaymentMethod] {
285 &self.inner.methods
286 }
287
288 fn inner(&self) -> &PaymentInstructionsImpl {
289 &self.inner
290 }
291}
292
293common_methods!(FixedAmountPaymentInstructions);
294
295#[derive(Clone, PartialEq, Eq, Debug)]
298pub struct ConfigurableAmountPaymentInstructions {
299 inner: PaymentInstructionsImpl,
300}
301
302impl ConfigurableAmountPaymentInstructions {
303 pub fn min_amt(&self) -> Option<Amount> {
306 self.inner.lnurl.as_ref().map(|(_, _, a, _)| *a)
307 }
308
309 pub fn max_amt(&self) -> Option<Amount> {
312 self.inner.lnurl.as_ref().map(|(_, _, _, a)| *a)
313 }
314
315 #[inline]
320 pub fn methods<'a>(&'a self) -> impl Iterator<Item = PossiblyResolvedPaymentMethod<'a>> {
321 let res = self.inner().methods.iter().map(PossiblyResolvedPaymentMethod::Resolved);
322 res.chain(self.inner().lnurl.iter().map(|(callback, _, min, max)| {
323 PossiblyResolvedPaymentMethod::LNURLPay { callback, min_value: *min, max_value: *max }
324 }))
325 }
326
327 pub async fn set_amount<R: HrnResolver>(
339 self, amount: Amount, resolver: &R,
340 ) -> Result<FixedAmountPaymentInstructions, &'static str> {
341 let mut inner = self.inner;
342 if let Some((callback, expected_desc_hash, min, max)) = inner.lnurl.take() {
343 if amount < min || amount > max {
344 return Err("Amount was not within the min_amt/max_amt bounds");
345 }
346 debug_assert!(inner.methods.is_empty());
347 debug_assert!(inner.onchain_amt.is_none());
348 debug_assert!(inner.pop_callback.is_none());
349 debug_assert!(inner.hrn_proof.is_none());
350 let bolt11 = resolver.resolve_lnurl(callback, amount, expected_desc_hash).await?;
351 if bolt11.amount_milli_satoshis() != Some(amount.milli_sats()) {
352 return Err("LNURL resolution resulted in a BOLT 11 invoice with the wrong amount");
353 }
354 inner.methods = vec![PaymentMethod::LightningBolt11(bolt11)];
355 inner.ln_amt = Some(amount);
356 } else {
357 if inner.methods.iter().any(|meth| matches!(meth, PaymentMethod::OnChain(_))) {
358 let amt = Amount::from_milli_sats((amount.milli_sats() + 999) / 1000)
359 .map_err(|_| "Requested amount was too close to 21M sats to round up")?;
360 inner.onchain_amt = Some(amt);
361 }
362 if inner.methods.iter().any(|meth| meth.is_lightning()) {
363 inner.ln_amt = Some(amount);
364 }
365 }
366 Ok(FixedAmountPaymentInstructions { inner })
367 }
368
369 fn inner(&self) -> &PaymentInstructionsImpl {
370 &self.inner
371 }
372}
373
374common_methods!(ConfigurableAmountPaymentInstructions);
375
376#[derive(Clone, PartialEq, Eq, Debug)]
387pub enum PaymentInstructions {
388 ConfigurableAmount(ConfigurableAmountPaymentInstructions),
396 FixedAmount(FixedAmountPaymentInstructions),
401}
402
403common_methods!(PaymentInstructions);
404
405impl PaymentInstructions {
406 fn inner(&self) -> &PaymentInstructionsImpl {
407 match self {
408 PaymentInstructions::ConfigurableAmount(inner) => &inner.inner,
409 PaymentInstructions::FixedAmount(inner) => &inner.inner,
410 }
411 }
412}
413
414pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats_panicy(100);
420
421#[derive(Debug)]
423#[cfg_attr(test, derive(PartialEq))]
424pub enum ParseError {
425 InvalidBolt11(ParseOrSemanticError),
427 InvalidBolt12(Bolt12ParseError),
429 InvalidOnChain(address::ParseError),
431 WrongNetwork,
433 InconsistentInstructions(&'static str),
438 InvalidInstructions(&'static str),
443 UnknownPaymentInstructions,
445 UnknownRequiredParameter,
447 HrnResolutionError(&'static str),
449 InstructionsExpired,
451}
452
453fn check_expiry(_expiry: Duration) -> Result<(), ParseError> {
454 #[cfg(feature = "std")]
455 {
456 use std::time::SystemTime;
457 if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
458 if now > _expiry {
459 return Err(ParseError::InstructionsExpired);
460 }
461 }
462 }
463 Ok(())
464}
465
466struct Bolt11Amounts {
467 ln_amount: Option<Amount>,
468 fallbacks_amount: Option<Amount>,
469}
470
471fn instructions_from_bolt11(
472 invoice: Bolt11Invoice, network: Network,
473) -> Result<(Option<String>, Bolt11Amounts, impl Iterator<Item = PaymentMethod>), ParseError> {
474 if invoice.network() != network {
475 return Err(ParseError::WrongNetwork);
476 }
477 if let Some(expiry) = invoice.expires_at() {
478 check_expiry(expiry)?;
479 }
480
481 let fallbacks = invoice.fallback_addresses().into_iter().map(PaymentMethod::OnChain);
482
483 let mut fallbacks_amount = None;
484 let mut ln_amount = None;
485 if let Some(amt_msat) = invoice.amount_milli_satoshis() {
486 let err = "BOLT 11 invoice required an amount greater than 21M BTC";
487 ln_amount = Some(
488 Amount::from_milli_sats(amt_msat).map_err(|_| ParseError::InvalidInstructions(err))?,
489 );
490 if !invoice.fallbacks().is_empty() {
491 fallbacks_amount = Some(
492 Amount::from_sats((amt_msat + 999) / 1000)
493 .map_err(|_| ParseError::InvalidInstructions(err))?,
494 );
495 }
496 }
497
498 let amounts = Bolt11Amounts { ln_amount, fallbacks_amount };
499
500 if let Bolt11InvoiceDescriptionRef::Direct(desc) = invoice.description() {
501 Ok((
502 Some(desc.as_inner().0.clone()),
503 amounts,
504 Some(PaymentMethod::LightningBolt11(invoice)).into_iter().chain(fallbacks),
505 ))
506 } else {
507 Ok((
508 None,
509 amounts,
510 Some(PaymentMethod::LightningBolt11(invoice)).into_iter().chain(fallbacks),
511 ))
512 }
513}
514
515fn check_offer(offer: Offer, net: Network) -> Result<(Option<String>, PaymentMethod), ParseError> {
516 if !offer.supports_chain(net.chain_hash()) {
517 return Err(ParseError::WrongNetwork);
518 }
519 if let Some(expiry) = offer.absolute_expiry() {
520 check_expiry(expiry)?;
521 }
522 let description = offer.description().map(|desc| desc.0.to_owned());
523 if let Some(offer::Amount::Bitcoin { amount_msats }) = offer.amount() {
524 if Amount::from_milli_sats(amount_msats).is_err() {
525 let err = "BOLT 12 offer requested an amount greater than 21M BTC";
526 return Err(ParseError::InvalidInstructions(err));
527 }
528 }
529 Ok((description, PaymentMethod::LightningBolt12(offer)))
530}
531
532fn split_once(haystack: &str, needle: char) -> (&str, Option<&str>) {
534 haystack.split_once(needle).map(|(a, b)| (a, Some(b))).unwrap_or((haystack, None))
535}
536
537fn un_percent_encode(encoded: &str) -> Result<String, ParseError> {
538 let mut res = Vec::with_capacity(encoded.len());
539 let mut iter = encoded.bytes();
540 let err = "A Proof of Payment URI was not properly %-encoded in a BIP 321 bitcoin: URI";
541 while let Some(b) = iter.next() {
542 if b == b'%' {
543 let high = iter.next().ok_or(ParseError::InvalidInstructions(err))?;
544 let low = iter.next().ok_or(ParseError::InvalidInstructions(err))?;
545 if !high.is_ascii_digit() || !low.is_ascii_digit() {
546 return Err(ParseError::InvalidInstructions(err));
547 }
548 res.push(((high - b'0') << 4) | (low - b'0'));
549 } else {
550 res.push(b);
551 }
552 }
553 String::from_utf8(res).map_err(|_| ParseError::InvalidInstructions(err))
554}
555
556#[test]
557fn test_un_percent_encode() {
558 assert_eq!(un_percent_encode("%20").unwrap(), " ");
559 assert_eq!(un_percent_encode("42%20 ").unwrap(), "42 ");
560 assert!(un_percent_encode("42%2").is_err());
561 assert!(un_percent_encode("42%2a").is_err());
562}
563
564fn parse_resolved_instructions(
565 instructions: &str, network: Network, supports_proof_of_payment_callbacks: bool,
566 hrn: Option<HumanReadableName>, hrn_proof: Option<Vec<u8>>,
567) -> Result<PaymentInstructions, ParseError> {
568 let (uri_proto, uri_suffix) = split_once(instructions, ':');
569
570 if uri_proto.eq_ignore_ascii_case("bitcoin") {
571 let (body, params) = split_once(uri_suffix.unwrap_or(""), '?');
572 let mut methods = Vec::new();
573 let mut description = None;
574 let mut pop_callback = None;
575 if !body.is_empty() {
576 let addr = Address::from_str(body).map_err(ParseError::InvalidOnChain)?;
577 let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
578 methods.push(PaymentMethod::OnChain(address));
579 }
580 if let Some(params) = params {
581 let mut onchain_amt = None;
582 for param in params.split('&') {
583 let (k, v) = split_once(param, '=');
584
585 let mut parse_segwit = |pfx| {
586 if let Some(address_string) = v {
587 if address_string.is_char_boundary(3)
588 && !address_string[..3].eq_ignore_ascii_case(pfx)
589 {
590 let err = "BIP 321 bitcoin: URI contained a bc/tb instruction which was not a Segwit address (bc1*/tb1*)";
593 return Err(ParseError::InvalidInstructions(err));
594 }
595 let addr = Address::from_str(address_string)
596 .map_err(ParseError::InvalidOnChain)?;
597 let address =
598 addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
599 methods.push(PaymentMethod::OnChain(address));
600 } else {
601 let err = "BIP 321 bitcoin: URI contained a bc (Segwit address) instruction without a value";
602 return Err(ParseError::InvalidInstructions(err));
603 }
604 Ok(())
605 };
606 if k.eq_ignore_ascii_case("bc") || k.eq_ignore_ascii_case("req-bc") {
607 parse_segwit("bc1")?;
608 } else if k.eq_ignore_ascii_case("tb") || k.eq_ignore_ascii_case("req-tb") {
609 parse_segwit("tb1")?;
610 } else if k.eq_ignore_ascii_case("lightning")
611 || k.eq_ignore_ascii_case("req-lightning")
612 {
613 if let Some(invoice_string) = v {
614 let invoice = Bolt11Invoice::from_str(invoice_string)
615 .map_err(ParseError::InvalidBolt11)?;
616 let (desc, amounts, method_iter) =
617 instructions_from_bolt11(invoice, network)?;
618 if let Some(fallbacks_amt) = amounts.fallbacks_amount {
619 if onchain_amt.is_some() && onchain_amt != Some(fallbacks_amt) {
620 let err = "BIP 321 bitcoin: URI contains lightning (BOLT 11 invoice) instructions with varying values";
621 return Err(ParseError::InconsistentInstructions(err));
622 }
623 onchain_amt = Some(fallbacks_amt);
624 }
625 if let Some(desc) = desc {
626 description = Some(desc);
627 }
628 for method in method_iter {
629 methods.push(method);
630 }
631 } else {
632 let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
633 return Err(ParseError::InvalidInstructions(err));
634 }
635 } else if k.eq_ignore_ascii_case("lno") || k.eq_ignore_ascii_case("req-lno") {
636 if let Some(offer_string) = v {
637 let offer =
638 Offer::from_str(offer_string).map_err(ParseError::InvalidBolt12)?;
639 let (desc, method) = check_offer(offer, network)?;
640 if let Some(desc) = desc {
641 description = Some(desc);
642 }
643 methods.push(method);
644 } else {
645 let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
646 return Err(ParseError::InvalidInstructions(err));
647 }
648 } else if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
649 } else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
651 } else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
653 {
654 } else if k.eq_ignore_ascii_case("pop") || k.eq_ignore_ascii_case("req-pop") {
656 if k.eq_ignore_ascii_case("req-pop") && !supports_proof_of_payment_callbacks {
657 return Err(ParseError::UnknownRequiredParameter);
658 }
659 if pop_callback.is_some() {
660 let err = "Multiple proof of payment callbacks appeared in a BIP 321 bitcoin: URI";
661 return Err(ParseError::InvalidInstructions(err));
662 }
663 if let Some(v) = v {
664 let callback_uri = un_percent_encode(v)?;
665 let (proto, _) = split_once(&callback_uri, ':');
666 let proto_isnt_local_app = proto.eq_ignore_ascii_case("javascript")
667 || proto.eq_ignore_ascii_case("http")
668 || proto.eq_ignore_ascii_case("https")
669 || proto.eq_ignore_ascii_case("file")
670 || proto.eq_ignore_ascii_case("mailto")
671 || proto.eq_ignore_ascii_case("ftp")
672 || proto.eq_ignore_ascii_case("wss")
673 || proto.eq_ignore_ascii_case("ws")
674 || proto.eq_ignore_ascii_case("ssh")
675 || proto.eq_ignore_ascii_case("tel") || proto.eq_ignore_ascii_case("data")
677 || proto.eq_ignore_ascii_case("blob");
678 if proto_isnt_local_app {
679 let err = "Proof of payment callback would not have opened a local app";
680 return Err(ParseError::InvalidInstructions(err));
681 }
682 pop_callback = Some(callback_uri);
683 } else {
684 let err = "Missing value for a Proof of Payment instruction in a BIP 321 bitcoin: URI";
685 return Err(ParseError::InvalidInstructions(err));
686 }
687 } else if k.is_char_boundary(4) && k[..4].eq_ignore_ascii_case("req-") {
688 return Err(ParseError::UnknownRequiredParameter);
689 }
690 }
691 let mut label = None;
692 let mut message = None;
693 let mut had_amt_param = false;
694 for param in params.split('&') {
695 let (k, v) = split_once(param, '=');
696 if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
697 if let Some(v) = v {
698 if had_amt_param {
699 let err = "Multiple amount parameters in a BIP 321 bitcoin: URI ";
700 return Err(ParseError::InvalidInstructions(err));
701 }
702 had_amt_param = true;
703
704 let err = "The amount parameter in a BIP 321 bitcoin: URI was invalid";
705 let btc_amt =
706 bitcoin::Amount::from_str_in(v, bitcoin::Denomination::Bitcoin)
707 .map_err(|_| ParseError::InvalidInstructions(err))?;
708
709 let err = "The amount parameter in a BIP 321 bitcoin: URI was greater than 21M BTC";
710 let amount = Amount::from_sats(btc_amt.to_sat())
711 .map_err(|_| ParseError::InvalidInstructions(err))?;
712
713 if onchain_amt.is_some() && onchain_amt != Some(amount) {
714 let err = "On-chain fallbacks from a lightning BOLT 11 invoice and the amount parameter in a BIP 321 bitcoin: URI differed in their amounts";
715 return Err(ParseError::InconsistentInstructions(err));
716 }
717 onchain_amt = Some(amount);
718 } else {
719 let err = "Missing value for an amount parameter in a BIP 321 bitcoin: URI";
720 return Err(ParseError::InvalidInstructions(err));
721 }
722 } else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
723 if label.is_some() {
724 let err = "Multiple label parameters in a BIP 321 bitcoin: URI";
725 return Err(ParseError::InvalidInstructions(err));
726 }
727 label = v;
728 } else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
729 {
730 if message.is_some() {
731 let err = "Multiple message parameters in a BIP 321 bitcoin: URI";
732 return Err(ParseError::InvalidInstructions(err));
733 }
734 message = v;
735 }
736 }
737
738 if methods.is_empty() {
739 return Err(ParseError::UnknownPaymentInstructions);
740 }
741
742 let mut min_amt = Amount::MAX;
743 let mut max_amt = Amount::ZERO;
744 let mut ln_amt = None;
745 let mut have_amountless_method = false;
746 let mut have_non_btc_denominated_method = false;
747 for method in methods.iter() {
748 let amt = match method {
749 PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
750 method.amount()
751 },
752 PaymentMethod::OnChain(_) => onchain_amt,
753 };
754 if let Some(amt) = amt {
755 if amt < min_amt {
756 min_amt = amt;
757 }
758 if amt > max_amt {
759 max_amt = amt;
760 }
761 match method {
762 PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
763 if let Some(ln_amt) = ln_amt {
764 if ln_amt != amt {
765 let err = "Had multiple different amounts in lightning payment methods in a BIP 321 bitcoin: URI";
766 return Err(ParseError::InconsistentInstructions(err));
767 }
768 }
769 ln_amt = Some(amt);
770 },
771 PaymentMethod::OnChain(_) => {},
772 }
773 } else if method.has_fixed_amount() {
774 have_non_btc_denominated_method = true;
775 } else {
776 have_amountless_method = true;
777 }
778 }
779 if have_amountless_method && have_non_btc_denominated_method {
780 let err = "Had some payment methods in a BIP 321 bitcoin: URI with required (non-BTC-denominated) amounts, some without";
781 return Err(ParseError::InconsistentInstructions(err));
782 }
783 let cant_have_amt = have_amountless_method || have_non_btc_denominated_method;
784 if (min_amt != Amount::MAX || max_amt != Amount::ZERO) && cant_have_amt {
785 let err = "Had some payment methods in a BIP 321 bitcoin: URI with required amounts, some without";
786 return Err(ParseError::InconsistentInstructions(err));
787 }
788 if max_amt.saturating_sub(min_amt) > MAX_AMOUNT_DIFFERENCE {
789 let err = "Payment methods differed in ";
790 return Err(ParseError::InconsistentInstructions(err));
791 }
792
793 let inner = PaymentInstructionsImpl {
794 description,
795 methods,
796 onchain_amt,
797 ln_amt,
798 lnurl: None,
799 pop_callback,
800 hrn,
801 hrn_proof,
802 };
803 if !have_amountless_method || have_non_btc_denominated_method {
804 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
805 } else {
806 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
807 inner,
808 }))
809 }
810 } else {
811 if methods.is_empty() {
813 Err(ParseError::UnknownPaymentInstructions)
814 } else {
815 let inner = PaymentInstructionsImpl {
816 description,
817 methods,
818 onchain_amt: None,
819 ln_amt: None,
820 lnurl: None,
821 pop_callback,
822 hrn,
823 hrn_proof,
824 };
825 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
826 inner,
827 }))
828 }
829 }
830 } else if uri_proto.eq_ignore_ascii_case("lightning") {
831 let invoice =
834 Bolt11Invoice::from_str(uri_suffix.unwrap_or("")).map_err(ParseError::InvalidBolt11)?;
835 let (description, amounts, method_iter) = instructions_from_bolt11(invoice, network)?;
836 let inner = PaymentInstructionsImpl {
837 description,
838 methods: method_iter.collect(),
839 onchain_amt: amounts.fallbacks_amount,
840 ln_amt: amounts.ln_amount,
841 lnurl: None,
842 pop_callback: None,
843 hrn,
844 hrn_proof,
845 };
846 if amounts.ln_amount.is_some() {
847 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
848 } else {
849 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
850 inner,
851 }))
852 }
853 } else if let Ok(addr) = Address::from_str(instructions) {
854 let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
855 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
856 inner: PaymentInstructionsImpl {
857 description: None,
858 methods: vec![PaymentMethod::OnChain(address)],
859 onchain_amt: None,
860 ln_amt: None,
861 lnurl: None,
862 pop_callback: None,
863 hrn,
864 hrn_proof,
865 },
866 }))
867 } else if let Ok(invoice) = Bolt11Invoice::from_str(instructions) {
868 let (description, amounts, method_iter) = instructions_from_bolt11(invoice, network)?;
869 let inner = PaymentInstructionsImpl {
870 description,
871 methods: method_iter.collect(),
872 onchain_amt: amounts.fallbacks_amount,
873 ln_amt: amounts.ln_amount,
874 lnurl: None,
875 pop_callback: None,
876 hrn,
877 hrn_proof,
878 };
879 if amounts.ln_amount.is_some() {
880 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
881 } else {
882 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
883 inner,
884 }))
885 }
886 } else if let Ok(offer) = Offer::from_str(instructions) {
887 let has_amt = offer.amount().is_some();
888 let (description, method) = check_offer(offer, network)?;
889 let inner = PaymentInstructionsImpl {
890 ln_amt: method.amount(),
891 description,
892 methods: vec![method],
893 onchain_amt: None,
894 lnurl: None,
895 pop_callback: None,
896 hrn,
897 hrn_proof,
898 };
899 if has_amt {
900 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
901 } else {
902 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
903 inner,
904 }))
905 }
906 } else {
907 Err(ParseError::UnknownPaymentInstructions)
908 }
909}
910
911impl PaymentInstructions {
912 pub async fn parse<H: HrnResolver>(
914 instructions: &str, network: Network, hrn_resolver: &H,
915 supports_proof_of_payment_callbacks: bool,
916 ) -> Result<PaymentInstructions, ParseError> {
917 let supports_pops = supports_proof_of_payment_callbacks;
918 if let Ok(hrn) = HumanReadableName::from_encoded(instructions) {
919 let resolution = hrn_resolver.resolve_hrn(&hrn).await;
920 let resolution = resolution.map_err(ParseError::HrnResolutionError)?;
921 match resolution {
922 HrnResolution::DNSSEC { proof, result } => {
923 parse_resolved_instructions(&result, network, supports_pops, Some(hrn), proof)
924 },
925 HrnResolution::LNURLPay {
926 min_value,
927 max_value,
928 expected_description_hash,
929 recipient_description,
930 callback,
931 } => {
932 let inner = PaymentInstructionsImpl {
933 description: recipient_description,
934 methods: Vec::new(),
935 lnurl: Some((callback, expected_description_hash, min_value, max_value)),
936 onchain_amt: None,
937 ln_amt: None,
938 pop_callback: None,
939 hrn: Some(hrn),
940 hrn_proof: None,
941 };
942 Ok(PaymentInstructions::ConfigurableAmount(
943 ConfigurableAmountPaymentInstructions { inner },
944 ))
945 },
946 }
947 } else {
948 parse_resolved_instructions(instructions, network, supports_pops, None, None)
949 }
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use alloc::format;
956 use alloc::str::FromStr;
957 #[cfg(not(feature = "std"))]
958 use alloc::string::ToString;
959
960 use super::*;
961
962 use crate::hrn_resolution::DummyHrnResolver;
963
964 const SAMPLE_INVOICE_WITH_FALLBACK: &str = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0";
965 const SAMPLE_INVOICE: &str = "lnbc20m1pn7qa2ndqqnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5kwzshmne5zw3lnfqdk8cv26mg9ndjapqzhcxn2wtn9d6ew5e2jfqsp5h3u5f0l522vs488h6n8zm5ca2lkpva532fnl2kp4wnvsuq445erq9qyysgqcqpcxqppz4395v2sjh3t5pzckgeelk9qf0z3fm9jzxtjqpqygayt4xyy7tpjvq5pe7f6727du2mg3t2tfe0cd53de2027ff7es7smtew8xx5x2spwuvkdz";
966 const SAMPLE_OFFER: &str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q";
967 const SAMPLE_BIP21: &str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz";
968
969 const SAMPLE_BIP21_WITH_INVOICE: &str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6";
970 #[cfg(not(feature = "std"))]
971 const SAMPLE_BIP21_WITH_INVOICE_ADDR: &str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u";
972 #[cfg(not(feature = "std"))]
973 const SAMPLE_BIP21_WITH_INVOICE_INVOICE: &str = "lnbc10u1p3pj257pp5yztkwjcz5ftl5laxkav23zmzekaw37zk6kmv80pk4xaev5qhtz7qdpdwd3xger9wd5kwm36yprx7u3qd36kucmgyp282etnv3shjcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqjcewm5cjwz4a6rfjx77c490yced6pemk0upkxhy89cmm7sct66k8gneanwykzgdrwrfje69h9u5u0w57rrcsysas7gadwmzxc8c6t0spjazup6";
974
975 const SAMPLE_BIP21_WITH_INVOICE_AND_LABEL: &str = "bitcoin:tb1p0vztr8q25czuka5u4ta5pqu0h8dxkf72mam89cpg4tg40fm8wgmqp3gv99?amount=0.000001&label=yooo&lightning=lntbs1u1pjrww6fdq809hk7mcnp4qvwggxr0fsueyrcer4x075walsv93vqvn3vlg9etesx287x6ddy4xpp5a3drwdx2fmkkgmuenpvmynnl7uf09jmgvtlg86ckkvgn99ajqgtssp5gr3aghgjxlwshnqwqn39c2cz5hw4cnsnzxdjn7kywl40rru4mjdq9qyysgqcqpcxqrpwurzjqfgtsj42x8an5zujpxvfhp9ngwm7u5lu8lvzfucjhex4pq8ysj5q2qqqqyqqv9cqqsqqqqlgqqqqqqqqfqzgl9zq04nzpxyvdr8vj3h98gvnj3luanj2cxcra0q2th4xjsxmtj8k3582l67xq9ffz5586f3nm5ax58xaqjg6rjcj2vzvx2q39v9eqpn0wx54";
976
977 #[tokio::test]
978 async fn parse_address() {
979 let addr_str = "1andreas3batLhQa2FawWjeyjCqyBzypd";
980 let parsed =
981 PaymentInstructions::parse(&addr_str, Network::Bitcoin, &DummyHrnResolver, false)
982 .await
983 .unwrap();
984
985 assert_eq!(parsed.recipient_description(), None);
986
987 let resolved = match parsed {
988 PaymentInstructions::ConfigurableAmount(parsed) => {
989 assert_eq!(parsed.min_amt(), None);
990 assert_eq!(parsed.min_amt(), None);
991 assert_eq!(parsed.methods().collect::<Vec<_>>().len(), 1);
992 parsed.set_amount(Amount::from_sats(10).unwrap(), &DummyHrnResolver).await.unwrap()
993 },
994 _ => panic!(),
995 };
996
997 assert_eq!(resolved.methods().len(), 1);
998 if let PaymentMethod::OnChain(address) = &resolved.methods()[0] {
999 assert_eq!(*address, Address::from_str(addr_str).unwrap().assume_checked());
1000 } else {
1001 panic!("Wrong method");
1002 }
1003 }
1004
1005 async fn check_ln_invoice(inv: &str) -> Result<PaymentInstructions, ParseError> {
1007 assert!(inv.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", inv);
1008 let resolver = &DummyHrnResolver;
1009 let raw = PaymentInstructions::parse(inv, Network::Bitcoin, resolver, false).await;
1010
1011 let ln_uri = format!("lightning:{}", inv);
1012 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1013 assert_eq!(raw, uri);
1014
1015 let ln_uri = format!("LIGHTNING:{}", inv);
1016 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1017 assert_eq!(raw, uri);
1018
1019 let ln_uri = ln_uri.to_uppercase();
1020 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1021 assert_eq!(raw, uri);
1022
1023 let btc_uri = format!("bitcoin:?lightning={}", inv);
1024 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1025 assert_eq!(raw, uri);
1026
1027 let btc_uri = btc_uri.to_uppercase();
1028 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1029 assert_eq!(raw, uri);
1030
1031 let btc_uri = format!("bitcoin:?req-lightning={}", inv);
1032 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1033 assert_eq!(raw, uri);
1034
1035 let btc_uri = btc_uri.to_uppercase();
1036 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1037 assert_eq!(raw, uri);
1038
1039 raw
1040 }
1041
1042 #[cfg(not(feature = "std"))]
1043 #[tokio::test]
1044 async fn parse_invoice() {
1045 let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE).unwrap();
1046 let parsed = check_ln_invoice(SAMPLE_INVOICE).await.unwrap();
1047
1048 let amt = invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap();
1049
1050 let parsed = match parsed {
1051 PaymentInstructions::FixedAmount(parsed) => parsed,
1052 _ => panic!(),
1053 };
1054
1055 assert_eq!(parsed.methods().len(), 1);
1056 assert_eq!(parsed.ln_payment_amount().unwrap(), amt);
1057 assert_eq!(parsed.onchain_payment_amount(), None);
1058 assert_eq!(parsed.max_amount().unwrap(), amt);
1059 assert_eq!(parsed.recipient_description(), Some(""));
1060 assert!(matches!(&parsed.methods()[0], &PaymentMethod::LightningBolt11(_)));
1061 }
1062
1063 #[cfg(feature = "std")]
1064 #[tokio::test]
1065 async fn parse_invoice() {
1066 assert_eq!(check_ln_invoice(SAMPLE_INVOICE).await, Err(ParseError::InstructionsExpired));
1067 }
1068
1069 #[cfg(not(feature = "std"))]
1070 #[tokio::test]
1071 async fn parse_invoice_with_fallback() {
1072 let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE_WITH_FALLBACK).unwrap();
1073 let parsed = check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await.unwrap();
1074
1075 let parsed = match parsed {
1076 PaymentInstructions::FixedAmount(parsed) => parsed,
1077 _ => panic!(),
1078 };
1079
1080 assert_eq!(parsed.methods().len(), 2);
1081 assert_eq!(
1082 parsed.max_amount().unwrap(),
1083 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1084 );
1085 assert_eq!(
1086 parsed.ln_payment_amount().unwrap(),
1087 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1088 );
1089 assert_eq!(
1090 parsed.onchain_payment_amount().unwrap(),
1091 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1092 );
1093
1094 assert_eq!(parsed.recipient_description(), None); let is_bolt11 = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::LightningBolt11(_));
1096 assert_eq!(parsed.methods().iter().filter(is_bolt11).count(), 1);
1097 let is_onchain = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::OnChain { .. });
1098 assert_eq!(parsed.methods().iter().filter(is_onchain).count(), 1);
1099 }
1100
1101 #[cfg(feature = "std")]
1102 #[tokio::test]
1103 async fn parse_invoice_with_fallback() {
1104 assert_eq!(
1105 check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await,
1106 Err(ParseError::InstructionsExpired),
1107 );
1108 }
1109
1110 async fn check_ln_offer(offer: &str) -> Result<PaymentInstructions, ParseError> {
1112 assert!(offer.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", offer);
1113 let resolver = &DummyHrnResolver;
1114 let raw = PaymentInstructions::parse(offer, Network::Signet, resolver, false).await;
1115
1116 let btc_uri = format!("bitcoin:?lno={}", offer);
1117 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1118 assert_eq!(raw, uri);
1119
1120 let btc_uri = btc_uri.to_uppercase();
1121 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1122 assert_eq!(raw, uri);
1123
1124 let btc_uri = format!("bitcoin:?req-lno={}", offer);
1125 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1126 assert_eq!(raw, uri);
1127
1128 let btc_uri = btc_uri.to_uppercase();
1129 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1130 assert_eq!(raw, uri);
1131
1132 raw
1133 }
1134
1135 #[tokio::test]
1136 async fn parse_offer() {
1137 let offer = Offer::from_str(SAMPLE_OFFER).unwrap();
1138 let amt_msats = match offer.amount() {
1139 None => None,
1140 Some(offer::Amount::Bitcoin { amount_msats }) => Some(amount_msats),
1141 Some(offer::Amount::Currency { .. }) => panic!(),
1142 };
1143 let parsed = check_ln_offer(SAMPLE_OFFER).await.unwrap();
1144
1145 let parsed = match parsed {
1146 PaymentInstructions::FixedAmount(parsed) => parsed,
1147 _ => panic!(),
1148 };
1149
1150 assert_eq!(parsed.methods().len(), 1);
1151 assert_eq!(
1152 parsed.methods()[0].amount().unwrap(),
1153 amt_msats.map(Amount::from_milli_sats).unwrap().unwrap()
1154 );
1155 assert_eq!(parsed.recipient_description(), Some("faucet"));
1156 assert!(matches!(parsed.methods()[0], PaymentMethod::LightningBolt12(_)));
1157 }
1158
1159 #[tokio::test]
1160 async fn parse_bip_21() {
1161 let parsed =
1162 PaymentInstructions::parse(SAMPLE_BIP21, Network::Bitcoin, &DummyHrnResolver, false)
1163 .await
1164 .unwrap();
1165
1166 assert_eq!(parsed.recipient_description(), None);
1167
1168 let parsed = match parsed {
1169 PaymentInstructions::FixedAmount(parsed) => parsed,
1170 _ => panic!(),
1171 };
1172
1173 let expected_amount = Amount::from_sats(5_000_000_000).unwrap();
1174
1175 assert_eq!(parsed.methods().len(), 1);
1176 assert_eq!(parsed.max_amount(), Some(expected_amount));
1177 assert_eq!(parsed.ln_payment_amount(), None);
1178 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1179 assert_eq!(parsed.recipient_description(), None);
1180 assert!(matches!(parsed.methods()[0], PaymentMethod::OnChain(_)));
1181 }
1182
1183 #[cfg(not(feature = "std"))]
1184 #[tokio::test]
1185 async fn parse_bip_21_with_invoice() {
1186 let parsed = PaymentInstructions::parse(
1187 SAMPLE_BIP21_WITH_INVOICE,
1188 Network::Bitcoin,
1189 &DummyHrnResolver,
1190 false,
1191 )
1192 .await
1193 .unwrap();
1194
1195 assert_eq!(parsed.recipient_description(), Some("sbddesign: For lunch Tuesday"));
1196
1197 let parsed = match parsed {
1198 PaymentInstructions::FixedAmount(parsed) => parsed,
1199 _ => panic!(),
1200 };
1201
1202 let expected_amount = Amount::from_milli_sats(1_000_000).unwrap();
1203
1204 assert_eq!(parsed.methods().len(), 2);
1205 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1206 assert_eq!(parsed.ln_payment_amount(), Some(expected_amount));
1207 assert_eq!(parsed.max_amount(), Some(expected_amount));
1208 assert_eq!(parsed.recipient_description(), Some("sbddesign: For lunch Tuesday"));
1209 if let PaymentMethod::OnChain(address) = &parsed.methods()[0] {
1210 assert_eq!(address.to_string(), SAMPLE_BIP21_WITH_INVOICE_ADDR);
1211 } else {
1212 panic!("Missing on-chain (or order changed)");
1213 }
1214 if let PaymentMethod::LightningBolt11(inv) = &parsed.methods()[1] {
1215 assert_eq!(inv.to_string(), SAMPLE_BIP21_WITH_INVOICE_INVOICE);
1216 } else {
1217 panic!("Missing invoice (or order changed)");
1218 }
1219 }
1220
1221 #[cfg(feature = "std")]
1222 #[tokio::test]
1223 async fn parse_bip_21_with_invoice() {
1224 assert_eq!(
1225 PaymentInstructions::parse(
1226 SAMPLE_BIP21_WITH_INVOICE,
1227 Network::Bitcoin,
1228 &DummyHrnResolver,
1229 false,
1230 )
1231 .await,
1232 Err(ParseError::InstructionsExpired),
1233 );
1234 }
1235
1236 #[cfg(not(feature = "std"))]
1237 #[tokio::test]
1238 async fn parse_bip_21_with_invoice_with_label() {
1239 let parsed = PaymentInstructions::parse(
1240 SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
1241 Network::Signet,
1242 &DummyHrnResolver,
1243 false,
1244 )
1245 .await
1246 .unwrap();
1247
1248 assert_eq!(parsed.recipient_description(), Some("yooo"));
1249
1250 let parsed = match parsed {
1251 PaymentInstructions::FixedAmount(parsed) => parsed,
1252 _ => panic!(),
1253 };
1254
1255 let expected_amount = Amount::from_milli_sats(100_000).unwrap();
1256
1257 assert_eq!(parsed.methods().len(), 2);
1258 assert_eq!(parsed.max_amount(), Some(expected_amount));
1259 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1260 assert_eq!(parsed.ln_payment_amount(), Some(expected_amount));
1261 assert_eq!(parsed.recipient_description(), Some("yooo"));
1262 assert!(matches!(parsed.methods()[0], PaymentMethod::OnChain(_)));
1263 assert!(matches!(parsed.methods()[1], PaymentMethod::LightningBolt11(_)));
1264 }
1265
1266 #[cfg(feature = "std")]
1267 #[tokio::test]
1268 async fn parse_bip_21_with_invoice_with_label() {
1269 assert_eq!(
1270 PaymentInstructions::parse(
1271 SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
1272 Network::Signet,
1273 &DummyHrnResolver,
1274 false,
1275 )
1276 .await,
1277 Err(ParseError::InstructionsExpired),
1278 );
1279 }
1280}