bitcoin_payment_instructions/
lib.rs

1//! These days, there are many possible ways to communicate Bitcoin payment instructions.
2//! This crate attempts to unify them into a simple parser which can read text provided directly by
3//! a payer or via a QR code scan/URI open and convert it into payment instructions.
4//!
5//! This crate doesn't actually help you *pay* these instructions, but provides a unified way to
6//! parse them.
7//!
8//! Payment instructions come in two versions -
9//!  * [`ConfigurableAmountPaymentInstructions`] represent instructions which can be paid with a
10//!    configurable amount, but may require further resolution to convert them into a
11//!    [`FixedAmountPaymentInstructions`] for payment.
12//!  * [`FixedAmountPaymentInstructions`] represent instructions for which the recipient wants a
13//!    specific quantity of funds and needs no further resolution
14//!
15//! In general, you should resolve a string (received either from a QR code scan, a system URI open
16//! call, a "recipient" text box, or a pasted "recipient" instruction) through
17//! [`PaymentInstructions::parse`].
18//!
19//! From there, if you receive a [`PaymentInstructions::FixedAmount`] you should check that you
20//! support at least one of the [`FixedAmountPaymentInstructions::methods`] and request approval
21//! from the wallet owner to complete the payment.
22//!
23//! If you receive a [`PaymentInstructions::ConfigurableAmount`] instead, you should similarly
24//! check that that you support one of the [`ConfigurableAmountPaymentInstructions::methods`] using
25//! [`PossiblyResolvedPaymentMethod::method_type`], then display an amount selection UI to the
26//! wallet owner. Once they've selected an amount, you should proceed with
27//! [`ConfigurableAmountPaymentInstructions::set_amount`] to fetch a finalized
28//! [`FixedAmountPaymentInstructions`] before moving to confirmation and payment.
29
30#![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/// A method which can be used to make a payment
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum PaymentMethod {
75	/// A payment using lightning as described by the given BOLT 11 invoice.
76	LightningBolt11(Bolt11Invoice),
77	/// A payment using lightning as described by the given BOLT 12 offer.
78	LightningBolt12(Offer),
79	/// A payment directly on-chain to the specified address.
80	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
127/// A payment method which may require further resolution once the amount we wish to pay is fixed.
128pub enum PossiblyResolvedPaymentMethod<'a> {
129	/// A payment using lightning as described by a BOLT 11 invoice which will be provided by this
130	/// LNURL-pay endpoint
131	LNURLPay {
132		/// The minimum value the recipient will accept payment for.
133		min_value: Amount,
134		/// The maximum value the recipient will accept payment for.
135		max_value: Amount,
136		/// The URI which must be fetched (once an `amount` parameter is added) to fully resolve
137		/// this into a [`Bolt11Invoice`].
138		callback: &'a str,
139	},
140	/// A payment method which has been fully resolved.
141	Resolved(&'a PaymentMethod),
142}
143
144/// The method that a [`PossiblyResolvedPaymentMethod`] will eventually resolve to.
145///
146/// This is useful to determine if you support the required payment mechanism for a
147/// [`ConfigurableAmountPaymentInstructions`] before you display an amount selector to the wallet
148/// owner.
149pub enum PaymentMethodType {
150	/// The [`PossiblyResolvedPaymentMethod`] will eventually resolve to a
151	/// [`PaymentMethod::LightningBolt11`].
152	LightningBolt11,
153	/// The [`PossiblyResolvedPaymentMethod`] will eventually resolve to a
154	/// [`PaymentMethod::LightningBolt12`].
155	LightningBolt12,
156	/// The [`PossiblyResolvedPaymentMethod`] will eventually resolve to a
157	/// [`PaymentMethod::OnChain`].
158	OnChain,
159}
160
161impl<'a> PossiblyResolvedPaymentMethod<'a> {
162	/// Fetches the [`PaymentMethodType`] that this payment method will ultimately resolve to.
163	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
185/// Defines common accessors for payment instructions in relation to [`PaymentInstructionsImpl`]
186macro_rules! common_methods {
187	($struct: ty) => {
188		impl $struct {
189			/// A recipient-provided description of the payment instructions.
190			///
191			/// This may be:
192			///  * the `label` or `message` parameter in a BIP 321 bitcoin: URI
193			///  * the `description` field in a lightning BOLT 11 invoice
194			///  * the `description` field in a lightning BOLT 12 offer
195			#[inline]
196			pub fn recipient_description(&self) -> Option<&str> {
197				self.inner().description.as_ref().map(|d| d.as_str())
198			}
199
200			/// Fetches the proof-of-payment callback URI.
201			///
202			/// Once a payment has been completed, the proof-of-payment (hex-encoded payment preimage for a
203			/// lightning BOLT 11 invoice, raw transaction serialized in hex for on-chain payments,
204			/// not-yet-defined for lightning BOLT 12 invoices) must be appended to this URI and the URI
205			/// opened with the default system URI handler.
206			#[inline]
207			pub fn pop_callback(&self) -> Option<&str> {
208				self.inner().pop_callback.as_ref().map(|c| c.as_str())
209			}
210
211			/// Fetches the [`HumanReadableName`] which was resolved, if the resolved payment instructions
212			/// were for a Human Readable Name.
213			#[inline]
214			pub fn human_readable_name(&self) -> &Option<HumanReadableName> {
215				&self.inner().hrn
216			}
217
218			/// Fetches the BIP 353 DNSSEC proof which was used to resolve these payment instructions, if
219			/// they were resolved from a HumanReadable Name using BIP 353.
220			///
221			/// This proof should be included in any PSBT output (as type `PSBT_OUT_DNSSEC_PROOF`)
222			/// generated using these payment instructions.
223			///
224			/// It should also be stored to allow us to later prove that this payment was made to
225			/// [`Self::human_readable_name`].
226			#[inline]
227			pub fn bip_353_dnssec_proof(&self) -> &Option<Vec<u8>> {
228				&self.inner().hrn_proof
229			}
230		}
231	};
232}
233
234/// Parsed payment instructions representing a set of possible ways to pay a fixed quantity to a
235/// recipient, as well as some associated metadata.
236#[derive(Clone, PartialEq, Eq, Debug)]
237pub struct FixedAmountPaymentInstructions {
238	inner: PaymentInstructionsImpl,
239}
240
241impl FixedAmountPaymentInstructions {
242	/// The maximum amount any payment instruction requires payment for.
243	///
244	/// If `None`, the only available payment method requires payment in a currency other than
245	/// sats, requiring currency conversion to determine the amount required.
246	///
247	/// Note that we may allow different [`Self::methods`] to have slightly different amounts (e.g.
248	/// if a recipient wishes to be paid more for on-chain payments to offset their future fees),
249	/// but only up to [`MAX_AMOUNT_DIFFERENCE`].
250	pub fn max_amount(&self) -> Option<Amount> {
251		core::cmp::max(self.inner.ln_amt, self.inner.onchain_amt)
252	}
253
254	/// The amount which the payment instruction requires payment for when paid over lightning.
255	///
256	/// We require that all lightning payment methods in payment instructions require an identical
257	/// amount for payment, and thus if this method returns `None` it indicates either:
258	///  * no lightning payment instructions exist,
259	///  * the only lightning payment instructions are for a BOLT 12 offer denominated in a
260	///    non-Bitcoin currency.
261	///
262	/// Note that if this object was built by resolving a [`ConfigurableAmountPaymentInstructions`]
263	/// with [`set_amount`] on a lightning BOLT 11 or BOLT 12 invoice-containing instruction, this
264	/// will return `Some` but the [`Self::methods`] with [`PaymentMethod::LightningBolt11`] or
265	/// [`PaymentMethod::LightningBolt12`] may still contain instructions without amounts.
266	///
267	/// [`set_amount`]: ConfigurableAmountPaymentInstructions::set_amount
268	pub fn ln_payment_amount(&self) -> Option<Amount> {
269		self.inner.ln_amt
270	}
271
272	/// The amount which the payment instruction requires payment for when paid on-chain.
273	///
274	/// Will return `None` if no on-chain payment instructions are available.
275	///
276	/// There is no way to encode different payment amounts for multiple on-chain formats
277	/// currently, and as such all on-chain [`PaymentMethod`]s are for the same amount.
278	pub fn onchain_payment_amount(&self) -> Option<Amount> {
279		self.inner.onchain_amt
280	}
281
282	/// The list of [`PaymentMethod`]s.
283	#[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/// Parsed payment instructions representing a set of possible ways to pay a configurable quantity
296/// of Bitcoin, as well as some associated metadata.
297#[derive(Clone, PartialEq, Eq, Debug)]
298pub struct ConfigurableAmountPaymentInstructions {
299	inner: PaymentInstructionsImpl,
300}
301
302impl ConfigurableAmountPaymentInstructions {
303	/// The minimum amount which the recipient will accept payment for, if provided as a part of
304	/// the payment instructions.
305	pub fn min_amt(&self) -> Option<Amount> {
306		self.inner.lnurl.as_ref().map(|(_, _, a, _)| *a)
307	}
308
309	/// The minimum amount which the recipient will accept payment for, if provided as a part of
310	/// the payment instructions.
311	pub fn max_amt(&self) -> Option<Amount> {
312		self.inner.lnurl.as_ref().map(|(_, _, _, a)| *a)
313	}
314
315	/// The supported list of [`PossiblyResolvedPaymentMethod`].
316	///
317	/// See [`PossiblyResolvedPaymentMethod::method_type`] for the specific payment protocol which
318	/// each payment method will ultimately resolve to.
319	#[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	/// Resolve the configurable amount to a fixed amount and create a
328	/// [`FixedAmountPaymentInstructions`].
329	///
330	/// May resolve LNURL-Pay instructions that were created from an LN-Address Human Readable
331	/// Name into a lightning [`Bolt11Invoice`].
332	///
333	/// Note that for lightning BOLT 11 or BOLT 12 instructions, we cannot modify the invoice/offer
334	/// itself and thus cannot set a specific amount on the [`PaymentMethod::LightningBolt11`] or
335	/// [`PaymentMethod::LightningBolt12`] inner fields themselves. Still,
336	/// [`FixedAmountPaymentInstructions::ln_payment_amount`] will return the value provided in
337	/// `amount`.
338	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/// Parsed payment instructions representing a set of possible ways to pay, as well as some
377/// associated metadata.
378///
379/// Currently we can resolve the following strings into payment instructions:
380///  * BIP 321 bitcoin: URIs
381///  * Lightning BOLT 11 invoices (optionally with the lightning: URI prefix)
382///  * Lightning BOLT 12 offers
383///  * On-chain addresses
384///  * BIP 353 human-readable names in the name@domain format.
385///  * LN-Address human-readable names in the name@domain format.
386#[derive(Clone, PartialEq, Eq, Debug)]
387pub enum PaymentInstructions {
388	/// The payment instructions support a variable amount which must be selected prior to payment.
389	///
390	/// In general, you should first check that you support some of the payment methods by calling
391	/// [`PossiblyResolvedPaymentMethod::method_type`] on each method in
392	/// [`ConfigurableAmountPaymentInstructions::methods`], then request the intended amount from
393	/// the wallet owner and build the final instructions using
394	/// [`ConfigurableAmountPaymentInstructions::set_amount`].
395	ConfigurableAmount(ConfigurableAmountPaymentInstructions),
396	/// The payment instructions support only payment for specific amount(s) given by
397	/// [`FixedAmountPaymentInstructions::ln_payment_amount`] and
398	/// [`FixedAmountPaymentInstructions::onchain_payment_amount`] (which are within
399	/// [`MAX_AMOUNT_DIFFERENCE`] of each other).
400	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
414/// The maximum amount requested that we will allow individual payment methods to differ in
415/// satoshis.
416///
417/// If any [`PaymentMethod`] is for an amount different by more than this amount from another
418/// [`PaymentMethod`], we will consider it a [`ParseError::InconsistentInstructions`].
419pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats_panicy(100);
420
421/// An error when parsing payment instructions into [`PaymentInstructions`].
422#[derive(Debug)]
423#[cfg_attr(test, derive(PartialEq))]
424pub enum ParseError {
425	/// An invalid lightning BOLT 11 invoice was encountered
426	InvalidBolt11(ParseOrSemanticError),
427	/// An invalid lightning BOLT 12 offer was encountered
428	InvalidBolt12(Bolt12ParseError),
429	/// An invalid on-chain address was encountered
430	InvalidOnChain(address::ParseError),
431	/// The payment instructions encoded instructions for a network other than the one specified.
432	WrongNetwork,
433	/// Different parts of the payment instructions were inconsistent.
434	///
435	/// A developer-readable error string is provided, though you may or may not wish to provide
436	/// this directly to users.
437	InconsistentInstructions(&'static str),
438	/// The instructions were invalid due to a semantic error.
439	///
440	/// A developer-readable error string is provided, though you may or may not wish to provide
441	/// this directly to users.
442	InvalidInstructions(&'static str),
443	/// The payment instructions did not appear to match any known form of payment instructions.
444	UnknownPaymentInstructions,
445	/// The BIP 321 bitcoin: URI included unknown required parameter(s)
446	UnknownRequiredParameter,
447	/// The call to [`HrnResolver::resolve_hrn`] failed with the contained error.
448	HrnResolutionError(&'static str),
449	/// The payment instructions have expired and are no longer payable.
450	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
532// What str.split_once() should do...
533fn 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							// `bc`/`tb` key-values must only include bech32/bech32m strings with
591							// HRP "bc"/"tb" (i.e. mainnet/testnet Segwit addresses).
592							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					// We handle this in the second loop below
650				} else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
651					// We handle this in the second loop below
652				} else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
653				{
654					// We handle this in the second loop below
655				} 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") // lol
676							|| 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			// No parameters were provided, so we just have the on-chain address in the URI body.
812			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		// Though there is no specification, lightning: URIs generally only include BOLT 11
832		// invoices.
833		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	/// Resolves a string into [`PaymentInstructions`].
913	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	// Test a handful of ways a lightning invoice might be communicated
1006	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); // no description for a description hash
1095		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	// Test a handful of ways a lightning offer might be communicated
1111	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}