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
60#[cfg(feature = "std")] pub mod onion_message_resolver;
62
63pub mod amount;
64
65pub mod receive;
66
67pub mod hrn_resolution;
68
69use amount::Amount;
70use hrn_resolution::{HrnResolution, HrnResolver, HumanReadableName};
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 =
351 resolver.resolve_lnurl_to_invoice(callback, amount, expected_desc_hash).await?;
352 if bolt11.amount_milli_satoshis() != Some(amount.milli_sats()) {
353 return Err("LNURL resolution resulted in a BOLT 11 invoice with the wrong amount");
354 }
355 inner.methods = vec![PaymentMethod::LightningBolt11(bolt11)];
356 inner.ln_amt = Some(amount);
357 } else {
358 if inner.methods.iter().any(|meth| matches!(meth, PaymentMethod::OnChain(_))) {
359 let amt = Amount::from_milli_sats((amount.milli_sats() + 999) / 1000)
360 .map_err(|_| "Requested amount was too close to 21M sats to round up")?;
361 inner.onchain_amt = Some(amt);
362 }
363 if inner.methods.iter().any(|meth| meth.is_lightning()) {
364 inner.ln_amt = Some(amount);
365 }
366 }
367 Ok(FixedAmountPaymentInstructions { inner })
368 }
369
370 fn inner(&self) -> &PaymentInstructionsImpl {
371 &self.inner
372 }
373}
374
375common_methods!(ConfigurableAmountPaymentInstructions);
376
377#[derive(Clone, PartialEq, Eq, Debug)]
388pub enum PaymentInstructions {
389 ConfigurableAmount(ConfigurableAmountPaymentInstructions),
397 FixedAmount(FixedAmountPaymentInstructions),
402}
403
404common_methods!(PaymentInstructions);
405
406impl PaymentInstructions {
407 fn inner(&self) -> &PaymentInstructionsImpl {
408 match self {
409 PaymentInstructions::ConfigurableAmount(inner) => &inner.inner,
410 PaymentInstructions::FixedAmount(inner) => &inner.inner,
411 }
412 }
413}
414
415pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats_panicy(100);
421
422#[derive(Debug)]
424#[cfg_attr(test, derive(PartialEq))]
425pub enum ParseError {
426 InvalidBolt11(ParseOrSemanticError),
428 InvalidBolt12(Bolt12ParseError),
430 InvalidOnChain(address::ParseError),
432 InvalidLnurl(&'static str),
434 WrongNetwork,
436 InconsistentInstructions(&'static str),
441 InvalidInstructions(&'static str),
446 UnknownPaymentInstructions,
448 UnknownRequiredParameter,
450 HrnResolutionError(&'static str),
452 InstructionsExpired,
454}
455
456fn check_expiry(_expiry: Duration) -> Result<(), ParseError> {
457 #[cfg(feature = "std")]
458 {
459 use std::time::SystemTime;
460 if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
461 if now > _expiry {
462 return Err(ParseError::InstructionsExpired);
463 }
464 }
465 }
466 Ok(())
467}
468
469struct Bolt11Amounts {
470 ln_amount: Option<Amount>,
471 fallbacks_amount: Option<Amount>,
472}
473
474fn instructions_from_bolt11(
475 invoice: Bolt11Invoice, network: Network,
476) -> Result<(Option<String>, Bolt11Amounts, impl Iterator<Item = PaymentMethod>), ParseError> {
477 if invoice.network() != network {
478 return Err(ParseError::WrongNetwork);
479 }
480 if let Some(expiry) = invoice.expires_at() {
481 check_expiry(expiry)?;
482 }
483
484 let fallbacks = invoice.fallback_addresses().into_iter().map(PaymentMethod::OnChain);
485
486 let mut fallbacks_amount = None;
487 let mut ln_amount = None;
488 if let Some(amt_msat) = invoice.amount_milli_satoshis() {
489 let err = "BOLT 11 invoice required an amount greater than 21M BTC";
490 ln_amount = Some(
491 Amount::from_milli_sats(amt_msat).map_err(|_| ParseError::InvalidInstructions(err))?,
492 );
493 if !invoice.fallbacks().is_empty() {
494 fallbacks_amount = Some(
495 Amount::from_sats((amt_msat + 999) / 1000)
496 .map_err(|_| ParseError::InvalidInstructions(err))?,
497 );
498 }
499 }
500
501 let amounts = Bolt11Amounts { ln_amount, fallbacks_amount };
502
503 if let Bolt11InvoiceDescriptionRef::Direct(desc) = invoice.description() {
504 Ok((
505 Some(desc.as_inner().0.clone()),
506 amounts,
507 Some(PaymentMethod::LightningBolt11(invoice)).into_iter().chain(fallbacks),
508 ))
509 } else {
510 Ok((
511 None,
512 amounts,
513 Some(PaymentMethod::LightningBolt11(invoice)).into_iter().chain(fallbacks),
514 ))
515 }
516}
517
518fn check_offer(offer: Offer, net: Network) -> Result<(Option<String>, PaymentMethod), ParseError> {
519 if !offer.supports_chain(net.chain_hash()) {
520 return Err(ParseError::WrongNetwork);
521 }
522 if let Some(expiry) = offer.absolute_expiry() {
523 check_expiry(expiry)?;
524 }
525 let description = offer.description().map(|desc| desc.0.to_owned());
526 if let Some(offer::Amount::Bitcoin { amount_msats }) = offer.amount() {
527 if Amount::from_milli_sats(amount_msats).is_err() {
528 let err = "BOLT 12 offer requested an amount greater than 21M BTC";
529 return Err(ParseError::InvalidInstructions(err));
530 }
531 }
532 Ok((description, PaymentMethod::LightningBolt12(offer)))
533}
534
535fn split_once(haystack: &str, needle: char) -> (&str, Option<&str>) {
537 haystack.split_once(needle).map(|(a, b)| (a, Some(b))).unwrap_or((haystack, None))
538}
539
540fn un_percent_encode(encoded: &str) -> Result<String, ParseError> {
541 let mut res = Vec::with_capacity(encoded.len());
542 let mut iter = encoded.bytes();
543 let err = "A Proof of Payment URI was not properly %-encoded in a BIP 321 bitcoin: URI";
544 while let Some(b) = iter.next() {
545 if b == b'%' {
546 let high = iter.next().ok_or(ParseError::InvalidInstructions(err))?;
547 let low = iter.next().ok_or(ParseError::InvalidInstructions(err))?;
548 if !high.is_ascii_digit() || !low.is_ascii_digit() {
549 return Err(ParseError::InvalidInstructions(err));
550 }
551 res.push(((high - b'0') << 4) | (low - b'0'));
552 } else {
553 res.push(b);
554 }
555 }
556 String::from_utf8(res).map_err(|_| ParseError::InvalidInstructions(err))
557}
558
559#[test]
560fn test_un_percent_encode() {
561 assert_eq!(un_percent_encode("%20").unwrap(), " ");
562 assert_eq!(un_percent_encode("42%20 ").unwrap(), "42 ");
563 assert!(un_percent_encode("42%2").is_err());
564 assert!(un_percent_encode("42%2a").is_err());
565}
566
567fn parse_resolved_instructions(
568 instructions: &str, network: Network, supports_proof_of_payment_callbacks: bool,
569 hrn: Option<HumanReadableName>, hrn_proof: Option<Vec<u8>>,
570) -> Result<PaymentInstructions, ParseError> {
571 let (uri_proto, uri_suffix) = split_once(instructions, ':');
572
573 if uri_proto.eq_ignore_ascii_case("bitcoin") {
574 let (body, params) = split_once(uri_suffix.unwrap_or(""), '?');
575 let mut methods = Vec::new();
576 let mut description = None;
577 let mut pop_callback = None;
578 if !body.is_empty() {
579 let addr = Address::from_str(body).map_err(ParseError::InvalidOnChain)?;
580 let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
581 methods.push(PaymentMethod::OnChain(address));
582 }
583 if let Some(params) = params {
584 let mut onchain_amt = None;
585 for param in params.split('&') {
586 let (k, v) = split_once(param, '=');
587
588 let mut parse_segwit = |pfx| {
589 if let Some(address_string) = v {
590 if address_string.is_char_boundary(3)
591 && !address_string[..3].eq_ignore_ascii_case(pfx)
592 {
593 let err = "BIP 321 bitcoin: URI contained a bc/tb instruction which was not a Segwit address (bc1*/tb1*)";
596 return Err(ParseError::InvalidInstructions(err));
597 }
598 let addr = Address::from_str(address_string)
599 .map_err(ParseError::InvalidOnChain)?;
600 let address =
601 addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
602 methods.push(PaymentMethod::OnChain(address));
603 } else {
604 let err = "BIP 321 bitcoin: URI contained a bc (Segwit address) instruction without a value";
605 return Err(ParseError::InvalidInstructions(err));
606 }
607 Ok(())
608 };
609 if k.eq_ignore_ascii_case("bc") || k.eq_ignore_ascii_case("req-bc") {
610 parse_segwit("bc1")?;
611 } else if k.eq_ignore_ascii_case("tb") || k.eq_ignore_ascii_case("req-tb") {
612 parse_segwit("tb1")?;
613 } else if k.eq_ignore_ascii_case("lightning")
614 || k.eq_ignore_ascii_case("req-lightning")
615 {
616 if let Some(invoice_string) = v {
617 let invoice = Bolt11Invoice::from_str(invoice_string)
618 .map_err(ParseError::InvalidBolt11)?;
619 let (desc, amounts, method_iter) =
620 instructions_from_bolt11(invoice, network)?;
621 if let Some(fallbacks_amt) = amounts.fallbacks_amount {
622 if onchain_amt.is_some() && onchain_amt != Some(fallbacks_amt) {
623 let err = "BIP 321 bitcoin: URI contains lightning (BOLT 11 invoice) instructions with varying values";
624 return Err(ParseError::InconsistentInstructions(err));
625 }
626 onchain_amt = Some(fallbacks_amt);
627 }
628 if let Some(desc) = desc {
629 description = Some(desc);
630 }
631 for method in method_iter {
632 methods.push(method);
633 }
634 } else {
635 let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
636 return Err(ParseError::InvalidInstructions(err));
637 }
638 } else if k.eq_ignore_ascii_case("lno") || k.eq_ignore_ascii_case("req-lno") {
639 if let Some(offer_string) = v {
640 let offer =
641 Offer::from_str(offer_string).map_err(ParseError::InvalidBolt12)?;
642 let (desc, method) = check_offer(offer, network)?;
643 if let Some(desc) = desc {
644 description = Some(desc);
645 }
646 methods.push(method);
647 } else {
648 let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
649 return Err(ParseError::InvalidInstructions(err));
650 }
651 } else if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
652 } else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
654 } else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
656 {
657 } else if k.eq_ignore_ascii_case("pop") || k.eq_ignore_ascii_case("req-pop") {
659 if k.eq_ignore_ascii_case("req-pop") && !supports_proof_of_payment_callbacks {
660 return Err(ParseError::UnknownRequiredParameter);
661 }
662 if pop_callback.is_some() {
663 let err = "Multiple proof of payment callbacks appeared in a BIP 321 bitcoin: URI";
664 return Err(ParseError::InvalidInstructions(err));
665 }
666 if let Some(v) = v {
667 let callback_uri = un_percent_encode(v)?;
668 let (proto, _) = split_once(&callback_uri, ':');
669 let proto_isnt_local_app = proto.eq_ignore_ascii_case("javascript")
670 || proto.eq_ignore_ascii_case("http")
671 || proto.eq_ignore_ascii_case("https")
672 || proto.eq_ignore_ascii_case("file")
673 || proto.eq_ignore_ascii_case("mailto")
674 || proto.eq_ignore_ascii_case("ftp")
675 || proto.eq_ignore_ascii_case("wss")
676 || proto.eq_ignore_ascii_case("ws")
677 || proto.eq_ignore_ascii_case("ssh")
678 || proto.eq_ignore_ascii_case("tel") || proto.eq_ignore_ascii_case("data")
680 || proto.eq_ignore_ascii_case("blob");
681 if proto_isnt_local_app {
682 let err = "Proof of payment callback would not have opened a local app";
683 return Err(ParseError::InvalidInstructions(err));
684 }
685 pop_callback = Some(callback_uri);
686 } else {
687 let err = "Missing value for a Proof of Payment instruction in a BIP 321 bitcoin: URI";
688 return Err(ParseError::InvalidInstructions(err));
689 }
690 } else if k.is_char_boundary(4) && k[..4].eq_ignore_ascii_case("req-") {
691 return Err(ParseError::UnknownRequiredParameter);
692 }
693 }
694 let mut label = None;
695 let mut message = None;
696 let mut had_amt_param = false;
697 for param in params.split('&') {
698 let (k, v) = split_once(param, '=');
699 if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
700 if let Some(v) = v {
701 if had_amt_param {
702 let err = "Multiple amount parameters in a BIP 321 bitcoin: URI ";
703 return Err(ParseError::InvalidInstructions(err));
704 }
705 had_amt_param = true;
706
707 let err = "The amount parameter in a BIP 321 bitcoin: URI was invalid";
708 let btc_amt =
709 bitcoin::Amount::from_str_in(v, bitcoin::Denomination::Bitcoin)
710 .map_err(|_| ParseError::InvalidInstructions(err))?;
711
712 let err = "The amount parameter in a BIP 321 bitcoin: URI was greater than 21M BTC";
713 let amount = Amount::from_sats(btc_amt.to_sat())
714 .map_err(|_| ParseError::InvalidInstructions(err))?;
715
716 if onchain_amt.is_some() && onchain_amt != Some(amount) {
717 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";
718 return Err(ParseError::InconsistentInstructions(err));
719 }
720 onchain_amt = Some(amount);
721 } else {
722 let err = "Missing value for an amount parameter in a BIP 321 bitcoin: URI";
723 return Err(ParseError::InvalidInstructions(err));
724 }
725 } else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
726 if label.is_some() {
727 let err = "Multiple label parameters in a BIP 321 bitcoin: URI";
728 return Err(ParseError::InvalidInstructions(err));
729 }
730 label = v;
731 } else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
732 {
733 if message.is_some() {
734 let err = "Multiple message parameters in a BIP 321 bitcoin: URI";
735 return Err(ParseError::InvalidInstructions(err));
736 }
737 message = v;
738 }
739 }
740
741 if methods.is_empty() {
742 return Err(ParseError::UnknownPaymentInstructions);
743 }
744
745 let mut min_amt = Amount::MAX;
746 let mut max_amt = Amount::ZERO;
747 let mut ln_amt = None;
748 let mut have_amountless_method = false;
749 let mut have_non_btc_denominated_method = false;
750 for method in methods.iter() {
751 let amt = match method {
752 PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
753 method.amount()
754 },
755 PaymentMethod::OnChain(_) => onchain_amt,
756 };
757 if let Some(amt) = amt {
758 if amt < min_amt {
759 min_amt = amt;
760 }
761 if amt > max_amt {
762 max_amt = amt;
763 }
764 match method {
765 PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
766 if let Some(ln_amt) = ln_amt {
767 if ln_amt != amt {
768 let err = "Had multiple different amounts in lightning payment methods in a BIP 321 bitcoin: URI";
769 return Err(ParseError::InconsistentInstructions(err));
770 }
771 }
772 ln_amt = Some(amt);
773 },
774 PaymentMethod::OnChain(_) => {},
775 }
776 } else if method.has_fixed_amount() {
777 have_non_btc_denominated_method = true;
778 } else {
779 have_amountless_method = true;
780 }
781 }
782 if have_amountless_method && have_non_btc_denominated_method {
783 let err = "Had some payment methods in a BIP 321 bitcoin: URI with required (non-BTC-denominated) amounts, some without";
784 return Err(ParseError::InconsistentInstructions(err));
785 }
786 let cant_have_amt = have_amountless_method || have_non_btc_denominated_method;
787 if (min_amt != Amount::MAX || max_amt != Amount::ZERO) && cant_have_amt {
788 let err = "Had some payment methods in a BIP 321 bitcoin: URI with required amounts, some without";
789 return Err(ParseError::InconsistentInstructions(err));
790 }
791 if max_amt.saturating_sub(min_amt) > MAX_AMOUNT_DIFFERENCE {
792 let err = "Payment methods differed in ";
793 return Err(ParseError::InconsistentInstructions(err));
794 }
795
796 let inner = PaymentInstructionsImpl {
797 description,
798 methods,
799 onchain_amt,
800 ln_amt,
801 lnurl: None,
802 pop_callback,
803 hrn,
804 hrn_proof,
805 };
806 if !have_amountless_method || have_non_btc_denominated_method {
807 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
808 } else {
809 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
810 inner,
811 }))
812 }
813 } else {
814 if methods.is_empty() {
816 Err(ParseError::UnknownPaymentInstructions)
817 } else {
818 let inner = PaymentInstructionsImpl {
819 description,
820 methods,
821 onchain_amt: None,
822 ln_amt: None,
823 lnurl: None,
824 pop_callback,
825 hrn,
826 hrn_proof,
827 };
828 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
829 inner,
830 }))
831 }
832 }
833 } else if uri_proto.eq_ignore_ascii_case("lightning") {
834 let invoice =
837 Bolt11Invoice::from_str(uri_suffix.unwrap_or("")).map_err(ParseError::InvalidBolt11)?;
838 let (description, amounts, method_iter) = instructions_from_bolt11(invoice, network)?;
839 let inner = PaymentInstructionsImpl {
840 description,
841 methods: method_iter.collect(),
842 onchain_amt: amounts.fallbacks_amount,
843 ln_amt: amounts.ln_amount,
844 lnurl: None,
845 pop_callback: None,
846 hrn,
847 hrn_proof,
848 };
849 if amounts.ln_amount.is_some() {
850 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
851 } else {
852 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
853 inner,
854 }))
855 }
856 } else if let Ok(addr) = Address::from_str(instructions) {
857 let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
858 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
859 inner: PaymentInstructionsImpl {
860 description: None,
861 methods: vec![PaymentMethod::OnChain(address)],
862 onchain_amt: None,
863 ln_amt: None,
864 lnurl: None,
865 pop_callback: None,
866 hrn,
867 hrn_proof,
868 },
869 }))
870 } else if let Ok(invoice) = Bolt11Invoice::from_str(instructions) {
871 let (description, amounts, method_iter) = instructions_from_bolt11(invoice, network)?;
872 let inner = PaymentInstructionsImpl {
873 description,
874 methods: method_iter.collect(),
875 onchain_amt: amounts.fallbacks_amount,
876 ln_amt: amounts.ln_amount,
877 lnurl: None,
878 pop_callback: None,
879 hrn,
880 hrn_proof,
881 };
882 if amounts.ln_amount.is_some() {
883 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
884 } else {
885 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
886 inner,
887 }))
888 }
889 } else if let Ok(offer) = Offer::from_str(instructions) {
890 let has_amt = offer.amount().is_some();
891 let (description, method) = check_offer(offer, network)?;
892 let inner = PaymentInstructionsImpl {
893 ln_amt: method.amount(),
894 description,
895 methods: vec![method],
896 onchain_amt: None,
897 lnurl: None,
898 pop_callback: None,
899 hrn,
900 hrn_proof,
901 };
902 if has_amt {
903 Ok(PaymentInstructions::FixedAmount(FixedAmountPaymentInstructions { inner }))
904 } else {
905 Ok(PaymentInstructions::ConfigurableAmount(ConfigurableAmountPaymentInstructions {
906 inner,
907 }))
908 }
909 } else {
910 Err(ParseError::UnknownPaymentInstructions)
911 }
912}
913
914impl PaymentInstructions {
915 pub async fn parse<H: HrnResolver>(
917 instructions: &str, network: Network, hrn_resolver: &H,
918 supports_proof_of_payment_callbacks: bool,
919 ) -> Result<PaymentInstructions, ParseError> {
920 let supports_pops = supports_proof_of_payment_callbacks;
921 let (uri_proto, _uri_suffix) = split_once(instructions, ':');
922
923 if let Ok(hrn) = HumanReadableName::from_encoded(instructions) {
924 let resolution = hrn_resolver.resolve_hrn(&hrn).await;
925 let resolution = resolution.map_err(ParseError::HrnResolutionError)?;
926 match resolution {
927 HrnResolution::DNSSEC { proof, result } => {
928 parse_resolved_instructions(&result, network, supports_pops, Some(hrn), proof)
929 },
930 HrnResolution::LNURLPay {
931 min_value,
932 max_value,
933 expected_description_hash,
934 recipient_description,
935 callback,
936 } => {
937 let inner = PaymentInstructionsImpl {
938 description: recipient_description,
939 methods: Vec::new(),
940 lnurl: Some((callback, expected_description_hash, min_value, max_value)),
941 onchain_amt: None,
942 ln_amt: None,
943 pop_callback: None,
944 hrn: Some(hrn),
945 hrn_proof: None,
946 };
947 Ok(PaymentInstructions::ConfigurableAmount(
948 ConfigurableAmountPaymentInstructions { inner },
949 ))
950 },
951 }
952 } else if uri_proto.eq_ignore_ascii_case("bitcoin:") {
953 parse_resolved_instructions(instructions, network, supports_pops, None, None)
956 } else if let Some(idx) = instructions.to_ascii_lowercase().rfind("lnurl") {
957 let mut lnurl_str = &instructions[idx..];
958 if let Some(idx) = lnurl_str.find('&') {
961 lnurl_str = &lnurl_str[..idx];
962 }
963 if let Some(idx) = lnurl_str.find('#') {
964 lnurl_str = &lnurl_str[..idx];
965 }
966 if let Ok((_, data)) = bitcoin::bech32::decode(lnurl_str) {
967 let url = String::from_utf8(data)
968 .map_err(|_| ParseError::InvalidLnurl("Not utf-8 encoded string"))?;
969 let resolution = hrn_resolver.resolve_lnurl(&url).await;
970 let resolution = resolution.map_err(ParseError::HrnResolutionError)?;
971 match resolution {
972 HrnResolution::DNSSEC { .. } => Err(ParseError::HrnResolutionError(
973 "Unexpected return when resolving lnurl",
974 )),
975 HrnResolution::LNURLPay {
976 min_value,
977 max_value,
978 expected_description_hash,
979 recipient_description,
980 callback,
981 } => {
982 let inner = PaymentInstructionsImpl {
983 description: recipient_description,
984 methods: Vec::new(),
985 lnurl: Some((
986 callback,
987 expected_description_hash,
988 min_value,
989 max_value,
990 )),
991 onchain_amt: None,
992 ln_amt: None,
993 pop_callback: None,
994 hrn: None,
995 hrn_proof: None,
996 };
997 Ok(PaymentInstructions::ConfigurableAmount(
998 ConfigurableAmountPaymentInstructions { inner },
999 ))
1000 },
1001 }
1002 } else {
1003 parse_resolved_instructions(instructions, network, supports_pops, None, None)
1004 }
1005 } else {
1006 parse_resolved_instructions(instructions, network, supports_pops, None, None)
1007 }
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use alloc::format;
1014 use alloc::str::FromStr;
1015 #[cfg(not(feature = "std"))]
1016 use alloc::string::ToString;
1017
1018 use super::*;
1019
1020 use crate::hrn_resolution::DummyHrnResolver;
1021
1022 const SAMPLE_INVOICE_WITH_FALLBACK: &str = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0";
1023 const SAMPLE_INVOICE: &str = "lnbc20m1pn7qa2ndqqnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5kwzshmne5zw3lnfqdk8cv26mg9ndjapqzhcxn2wtn9d6ew5e2jfqsp5h3u5f0l522vs488h6n8zm5ca2lkpva532fnl2kp4wnvsuq445erq9qyysgqcqpcxqppz4395v2sjh3t5pzckgeelk9qf0z3fm9jzxtjqpqygayt4xyy7tpjvq5pe7f6727du2mg3t2tfe0cd53de2027ff7es7smtew8xx5x2spwuvkdz";
1024 const SAMPLE_OFFER: &str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q";
1025 const SAMPLE_BIP21: &str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz";
1026
1027 #[cfg(feature = "http")]
1028 const SAMPLE_LNURL: &str = "LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK";
1029 #[cfg(feature = "http")]
1030 const SAMPLE_LNURL_LN_PREFIX: &str = "lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK";
1031 #[cfg(feature = "http")]
1032 const SAMPLE_LNURL_FALLBACK: &str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK";
1033 #[cfg(feature = "http")]
1034 const SAMPLE_LNURL_FALLBACK_WITH_AND: &str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK&extra=my_extra_param";
1035 #[cfg(feature = "http")]
1036 const SAMPLE_LNURL_FALLBACK_WITH_HASHTAG: &str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK#extra=my_extra_param";
1037 #[cfg(feature = "http")]
1038 const SAMPLE_LNURL_FALLBACK_WITH_BOTH: &str = "https://service.com/giftcard/redeem?id=123&lightning=LNURL1DP68GURN8GHJ7MRWW4EXCTNDW46XJMNEDEJHGTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHHYETXW4HXG0AH8NK&extra=my_extra_param#extra2=another_extra_param";
1039
1040 const SAMPLE_BIP21_WITH_INVOICE: &str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6";
1041 #[cfg(not(feature = "std"))]
1042 const SAMPLE_BIP21_WITH_INVOICE_ADDR: &str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u";
1043 #[cfg(not(feature = "std"))]
1044 const SAMPLE_BIP21_WITH_INVOICE_INVOICE: &str = "lnbc10u1p3pj257pp5yztkwjcz5ftl5laxkav23zmzekaw37zk6kmv80pk4xaev5qhtz7qdpdwd3xger9wd5kwm36yprx7u3qd36kucmgyp282etnv3shjcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqjcewm5cjwz4a6rfjx77c490yced6pemk0upkxhy89cmm7sct66k8gneanwykzgdrwrfje69h9u5u0w57rrcsysas7gadwmzxc8c6t0spjazup6";
1045
1046 const SAMPLE_BIP21_WITH_INVOICE_AND_LABEL: &str = "bitcoin:tb1p0vztr8q25czuka5u4ta5pqu0h8dxkf72mam89cpg4tg40fm8wgmqp3gv99?amount=0.000001&label=yooo&lightning=lntbs1u1pjrww6fdq809hk7mcnp4qvwggxr0fsueyrcer4x075walsv93vqvn3vlg9etesx287x6ddy4xpp5a3drwdx2fmkkgmuenpvmynnl7uf09jmgvtlg86ckkvgn99ajqgtssp5gr3aghgjxlwshnqwqn39c2cz5hw4cnsnzxdjn7kywl40rru4mjdq9qyysgqcqpcxqrpwurzjqfgtsj42x8an5zujpxvfhp9ngwm7u5lu8lvzfucjhex4pq8ysj5q2qqqqyqqv9cqqsqqqqlgqqqqqqqqfqzgl9zq04nzpxyvdr8vj3h98gvnj3luanj2cxcra0q2th4xjsxmtj8k3582l67xq9ffz5586f3nm5ax58xaqjg6rjcj2vzvx2q39v9eqpn0wx54";
1047
1048 #[tokio::test]
1049 async fn parse_address() {
1050 let addr_str = "1andreas3batLhQa2FawWjeyjCqyBzypd";
1051 let parsed =
1052 PaymentInstructions::parse(&addr_str, Network::Bitcoin, &DummyHrnResolver, false)
1053 .await
1054 .unwrap();
1055
1056 assert_eq!(parsed.recipient_description(), None);
1057
1058 let resolved = match parsed {
1059 PaymentInstructions::ConfigurableAmount(parsed) => {
1060 assert_eq!(parsed.min_amt(), None);
1061 assert_eq!(parsed.min_amt(), None);
1062 assert_eq!(parsed.methods().collect::<Vec<_>>().len(), 1);
1063 parsed.set_amount(Amount::from_sats(10).unwrap(), &DummyHrnResolver).await.unwrap()
1064 },
1065 _ => panic!(),
1066 };
1067
1068 assert_eq!(resolved.methods().len(), 1);
1069 if let PaymentMethod::OnChain(address) = &resolved.methods()[0] {
1070 assert_eq!(*address, Address::from_str(addr_str).unwrap().assume_checked());
1071 } else {
1072 panic!("Wrong method");
1073 }
1074 }
1075
1076 async fn check_ln_invoice(inv: &str) -> Result<PaymentInstructions, ParseError> {
1078 assert!(inv.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", inv);
1079 let resolver = &DummyHrnResolver;
1080 let raw = PaymentInstructions::parse(inv, Network::Bitcoin, resolver, false).await;
1081
1082 let ln_uri = format!("lightning:{}", inv);
1083 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1084 assert_eq!(raw, uri);
1085
1086 let ln_uri = format!("LIGHTNING:{}", inv);
1087 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1088 assert_eq!(raw, uri);
1089
1090 let ln_uri = ln_uri.to_uppercase();
1091 let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
1092 assert_eq!(raw, uri);
1093
1094 let btc_uri = format!("bitcoin:?lightning={}", inv);
1095 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1096 assert_eq!(raw, uri);
1097
1098 let btc_uri = btc_uri.to_uppercase();
1099 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1100 assert_eq!(raw, uri);
1101
1102 let btc_uri = format!("bitcoin:?req-lightning={}", inv);
1103 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1104 assert_eq!(raw, uri);
1105
1106 let btc_uri = btc_uri.to_uppercase();
1107 let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
1108 assert_eq!(raw, uri);
1109
1110 raw
1111 }
1112
1113 #[cfg(not(feature = "std"))]
1114 #[tokio::test]
1115 async fn parse_invoice() {
1116 let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE).unwrap();
1117 let parsed = check_ln_invoice(SAMPLE_INVOICE).await.unwrap();
1118
1119 let amt = invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap();
1120
1121 let parsed = match parsed {
1122 PaymentInstructions::FixedAmount(parsed) => parsed,
1123 _ => panic!(),
1124 };
1125
1126 assert_eq!(parsed.methods().len(), 1);
1127 assert_eq!(parsed.ln_payment_amount().unwrap(), amt);
1128 assert_eq!(parsed.onchain_payment_amount(), None);
1129 assert_eq!(parsed.max_amount().unwrap(), amt);
1130 assert_eq!(parsed.recipient_description(), Some(""));
1131 assert!(matches!(&parsed.methods()[0], &PaymentMethod::LightningBolt11(_)));
1132 }
1133
1134 #[cfg(feature = "std")]
1135 #[tokio::test]
1136 async fn parse_invoice() {
1137 assert_eq!(check_ln_invoice(SAMPLE_INVOICE).await, Err(ParseError::InstructionsExpired));
1138 }
1139
1140 #[cfg(not(feature = "std"))]
1141 #[tokio::test]
1142 async fn parse_invoice_with_fallback() {
1143 let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE_WITH_FALLBACK).unwrap();
1144 let parsed = check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await.unwrap();
1145
1146 let parsed = match parsed {
1147 PaymentInstructions::FixedAmount(parsed) => parsed,
1148 _ => panic!(),
1149 };
1150
1151 assert_eq!(parsed.methods().len(), 2);
1152 assert_eq!(
1153 parsed.max_amount().unwrap(),
1154 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1155 );
1156 assert_eq!(
1157 parsed.ln_payment_amount().unwrap(),
1158 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1159 );
1160 assert_eq!(
1161 parsed.onchain_payment_amount().unwrap(),
1162 invoice.amount_milli_satoshis().map(Amount::from_milli_sats).unwrap().unwrap(),
1163 );
1164
1165 assert_eq!(parsed.recipient_description(), None); let is_bolt11 = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::LightningBolt11(_));
1167 assert_eq!(parsed.methods().iter().filter(is_bolt11).count(), 1);
1168 let is_onchain = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::OnChain { .. });
1169 assert_eq!(parsed.methods().iter().filter(is_onchain).count(), 1);
1170 }
1171
1172 #[cfg(feature = "std")]
1173 #[tokio::test]
1174 async fn parse_invoice_with_fallback() {
1175 assert_eq!(
1176 check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await,
1177 Err(ParseError::InstructionsExpired),
1178 );
1179 }
1180
1181 async fn check_ln_offer(offer: &str) -> Result<PaymentInstructions, ParseError> {
1183 assert!(offer.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", offer);
1184 let resolver = &DummyHrnResolver;
1185 let raw = PaymentInstructions::parse(offer, Network::Signet, resolver, false).await;
1186
1187 let btc_uri = format!("bitcoin:?lno={}", offer);
1188 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1189 assert_eq!(raw, uri);
1190
1191 let btc_uri = btc_uri.to_uppercase();
1192 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1193 assert_eq!(raw, uri);
1194
1195 let btc_uri = format!("bitcoin:?req-lno={}", offer);
1196 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1197 assert_eq!(raw, uri);
1198
1199 let btc_uri = btc_uri.to_uppercase();
1200 let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
1201 assert_eq!(raw, uri);
1202
1203 raw
1204 }
1205
1206 #[tokio::test]
1207 async fn parse_offer() {
1208 let offer = Offer::from_str(SAMPLE_OFFER).unwrap();
1209 let amt_msats = match offer.amount() {
1210 None => None,
1211 Some(offer::Amount::Bitcoin { amount_msats }) => Some(amount_msats),
1212 Some(offer::Amount::Currency { .. }) => panic!(),
1213 };
1214 let parsed = check_ln_offer(SAMPLE_OFFER).await.unwrap();
1215
1216 let parsed = match parsed {
1217 PaymentInstructions::FixedAmount(parsed) => parsed,
1218 _ => panic!(),
1219 };
1220
1221 assert_eq!(parsed.methods().len(), 1);
1222 assert_eq!(
1223 parsed.methods()[0].amount().unwrap(),
1224 amt_msats.map(Amount::from_milli_sats).unwrap().unwrap()
1225 );
1226 assert_eq!(parsed.recipient_description(), Some("faucet"));
1227 assert!(matches!(parsed.methods()[0], PaymentMethod::LightningBolt12(_)));
1228 }
1229
1230 #[tokio::test]
1231 async fn parse_bip_21() {
1232 let parsed =
1233 PaymentInstructions::parse(SAMPLE_BIP21, Network::Bitcoin, &DummyHrnResolver, false)
1234 .await
1235 .unwrap();
1236
1237 assert_eq!(parsed.recipient_description(), None);
1238
1239 let parsed = match parsed {
1240 PaymentInstructions::FixedAmount(parsed) => parsed,
1241 _ => panic!(),
1242 };
1243
1244 let expected_amount = Amount::from_sats(5_000_000_000).unwrap();
1245
1246 assert_eq!(parsed.methods().len(), 1);
1247 assert_eq!(parsed.max_amount(), Some(expected_amount));
1248 assert_eq!(parsed.ln_payment_amount(), None);
1249 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1250 assert_eq!(parsed.recipient_description(), None);
1251 assert!(matches!(parsed.methods()[0], PaymentMethod::OnChain(_)));
1252 }
1253
1254 #[cfg(not(feature = "std"))]
1255 #[tokio::test]
1256 async fn parse_bip_21_with_invoice() {
1257 let parsed = PaymentInstructions::parse(
1258 SAMPLE_BIP21_WITH_INVOICE,
1259 Network::Bitcoin,
1260 &DummyHrnResolver,
1261 false,
1262 )
1263 .await
1264 .unwrap();
1265
1266 assert_eq!(parsed.recipient_description(), Some("sbddesign: For lunch Tuesday"));
1267
1268 let parsed = match parsed {
1269 PaymentInstructions::FixedAmount(parsed) => parsed,
1270 _ => panic!(),
1271 };
1272
1273 let expected_amount = Amount::from_milli_sats(1_000_000).unwrap();
1274
1275 assert_eq!(parsed.methods().len(), 2);
1276 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1277 assert_eq!(parsed.ln_payment_amount(), Some(expected_amount));
1278 assert_eq!(parsed.max_amount(), Some(expected_amount));
1279 assert_eq!(parsed.recipient_description(), Some("sbddesign: For lunch Tuesday"));
1280 if let PaymentMethod::OnChain(address) = &parsed.methods()[0] {
1281 assert_eq!(address.to_string(), SAMPLE_BIP21_WITH_INVOICE_ADDR);
1282 } else {
1283 panic!("Missing on-chain (or order changed)");
1284 }
1285 if let PaymentMethod::LightningBolt11(inv) = &parsed.methods()[1] {
1286 assert_eq!(inv.to_string(), SAMPLE_BIP21_WITH_INVOICE_INVOICE);
1287 } else {
1288 panic!("Missing invoice (or order changed)");
1289 }
1290 }
1291
1292 #[cfg(feature = "std")]
1293 #[tokio::test]
1294 async fn parse_bip_21_with_invoice() {
1295 assert_eq!(
1296 PaymentInstructions::parse(
1297 SAMPLE_BIP21_WITH_INVOICE,
1298 Network::Bitcoin,
1299 &DummyHrnResolver,
1300 false,
1301 )
1302 .await,
1303 Err(ParseError::InstructionsExpired),
1304 );
1305 }
1306
1307 #[cfg(not(feature = "std"))]
1308 #[tokio::test]
1309 async fn parse_bip_21_with_invoice_with_label() {
1310 let parsed = PaymentInstructions::parse(
1311 SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
1312 Network::Signet,
1313 &DummyHrnResolver,
1314 false,
1315 )
1316 .await
1317 .unwrap();
1318
1319 assert_eq!(parsed.recipient_description(), Some("yooo"));
1320
1321 let parsed = match parsed {
1322 PaymentInstructions::FixedAmount(parsed) => parsed,
1323 _ => panic!(),
1324 };
1325
1326 let expected_amount = Amount::from_milli_sats(100_000).unwrap();
1327
1328 assert_eq!(parsed.methods().len(), 2);
1329 assert_eq!(parsed.max_amount(), Some(expected_amount));
1330 assert_eq!(parsed.onchain_payment_amount(), Some(expected_amount));
1331 assert_eq!(parsed.ln_payment_amount(), Some(expected_amount));
1332 assert_eq!(parsed.recipient_description(), Some("yooo"));
1333 assert!(matches!(parsed.methods()[0], PaymentMethod::OnChain(_)));
1334 assert!(matches!(parsed.methods()[1], PaymentMethod::LightningBolt11(_)));
1335 }
1336
1337 #[cfg(feature = "std")]
1338 #[tokio::test]
1339 async fn parse_bip_21_with_invoice_with_label() {
1340 assert_eq!(
1341 PaymentInstructions::parse(
1342 SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
1343 Network::Signet,
1344 &DummyHrnResolver,
1345 false,
1346 )
1347 .await,
1348 Err(ParseError::InstructionsExpired),
1349 );
1350 }
1351
1352 #[cfg(feature = "http")]
1353 async fn test_lnurl(str: &str) {
1354 let resolver = http_resolver::HTTPHrnResolver::default();
1355 let parsed =
1356 PaymentInstructions::parse(str, Network::Signet, &resolver, false).await.unwrap();
1357
1358 let parsed = match parsed {
1359 PaymentInstructions::ConfigurableAmount(parsed) => parsed,
1360 _ => panic!(),
1361 };
1362
1363 assert_eq!(parsed.methods().count(), 1);
1364 assert_eq!(parsed.min_amt(), Some(Amount::from_milli_sats(1000).unwrap()));
1365 assert_eq!(parsed.max_amt(), Some(Amount::from_milli_sats(11000000000).unwrap()));
1366 }
1367
1368 #[cfg(feature = "http")]
1369 #[tokio::test]
1370 async fn parse_lnurl() {
1371 test_lnurl(SAMPLE_LNURL).await;
1372 test_lnurl(SAMPLE_LNURL_LN_PREFIX).await;
1373 test_lnurl(SAMPLE_LNURL_FALLBACK).await;
1374 test_lnurl(SAMPLE_LNURL_FALLBACK_WITH_AND).await;
1375 test_lnurl(SAMPLE_LNURL_FALLBACK_WITH_HASHTAG).await;
1376 test_lnurl(SAMPLE_LNURL_FALLBACK_WITH_BOTH).await;
1377 }
1378}