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//! See the [`PaymentInstructions`] type for the supported instruction formats.
6//!
7//! This crate doesn't actually help you *pay* these instructions, but provides a unified way to
8//! parse them.
9
10// TODO: We should also be able to parse refund instructions, either ln-address ones or bolt 12
11// refunds or on-chain private key for sweep
12
13#![deny(missing_docs)]
14#![forbid(unsafe_code)]
15#![deny(rustdoc::broken_intra_doc_links)]
16#![deny(rustdoc::private_intra_doc_links)]
17#![cfg_attr(not(feature = "std"), no_std)]
18
19extern crate alloc;
20extern crate core;
21
22use alloc::borrow::ToOwned;
23use alloc::boxed::Box;
24use alloc::str::FromStr;
25use alloc::string::String;
26use alloc::vec;
27use alloc::vec::Vec;
28
29use core::future::Future;
30use core::pin::Pin;
31
32use bitcoin::{address, Address, Network};
33use lightning::offers::offer::{self, Offer};
34use lightning::offers::parse::Bolt12ParseError;
35use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef, ParseOrSemanticError};
36
37pub use lightning::onion_message::dns_resolution::HumanReadableName;
38
39#[cfg(feature = "std")]
40pub mod onion_message_resolver;
41
42#[cfg(feature = "http")]
43pub mod http_resolver;
44
45pub mod amount;
46use amount::Amount;
47
48/// A method which can be used to make a payment
49#[derive(PartialEq, Eq, Debug)]
50pub enum PaymentMethod {
51	/// A payment using lightning as descibred by the given BOLT 11 invoice.
52	LightningBolt11(Bolt11Invoice),
53	/// A payment using lightning as descibred by the given BOLT 12 offer.
54	LightningBolt12(Offer),
55	/// A payment directly on-chain to the specified address.
56	OnChain {
57		/// The amount which this payment method requires payment for.
58		///
59		/// * For instructions extracted from BIP 321 bitcoin: URIs this is the `amount` parameter.
60		/// * For the fallback address from a lightning BOLT 11 invoice this is the invoice's
61		///   amount, rounded up to the nearest whole satoshi.
62		amount: Option<Amount>,
63		/// The address to which payment can be made.
64		address: Address,
65	},
66}
67
68impl PaymentMethod {
69	/// The amount this payment method requires payment for.
70	///
71	/// If `None` for non-BOLT 12 payments, any amount can be paid.
72	///
73	/// For Lightning BOLT 12 offers, the requested amount may be denominated in an alternative
74	/// currency, requiring currency conversion and negotiatin while paying. In such a case, `None`
75	/// will be returned. See [`Offer::amount`] and LDK's offer payment logic for more info.
76	pub fn amount(&self) -> Option<Amount> {
77		match self {
78			PaymentMethod::LightningBolt11(invoice) => {
79				invoice.amount_milli_satoshis().map(|a| Amount::from_milli_sats(a))
80			},
81			PaymentMethod::LightningBolt12(offer) => match offer.amount() {
82				Some(offer::Amount::Bitcoin { amount_msats }) => {
83					Some(Amount::from_milli_sats(amount_msats))
84				},
85				Some(offer::Amount::Currency { .. }) => None,
86				None => None,
87			},
88			PaymentMethod::OnChain { amount, .. } => *amount,
89		}
90	}
91}
92
93/// Parsed payment instructions representing a set of possible ways to pay, as well as some
94/// associated metadata.
95///
96/// It supports:
97///  * BIP 321 bitcoin: URIs
98///  * Lightning BOLT 11 invoices (optionally with the lightning: URI prefix)
99///  * Lightning BOLT 12 offers
100///  * On-chain addresses
101///  * BIP 353 human-readable names in the name@domain format.
102///  * LN-Address human-readable names in the name@domain format.
103#[derive(PartialEq, Eq, Debug)]
104pub struct PaymentInstructions {
105	recipient_description: Option<String>,
106	methods: Vec<PaymentMethod>,
107	pop_callback: Option<String>,
108	hrn: Option<HumanReadableName>,
109	hrn_proof: Option<Vec<u8>>,
110}
111
112/// The maximum amount requested that we will allow individual payment methods to differ in
113/// satoshis.
114///
115/// If any [`PaymentMethod::amount`] differs from another by more than this amount, we will
116/// consider it a [`ParseError::InconsistentInstructions`].
117pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats(100);
118
119/// An error when parsing payment instructions into [`PaymentInstructions`].
120#[derive(Debug)]
121pub enum ParseError {
122	/// An invalid lightning BOLT 11 invoice was encountered
123	InvalidBolt11(ParseOrSemanticError),
124	/// An invalid lightning BOLT 12 offer was encountered
125	InvalidBolt12(Bolt12ParseError),
126	/// An invalid on-chain address was encountered
127	InvalidOnChain(address::ParseError),
128	/// The payment instructions encoded instructions for a network other than the one specified.
129	WrongNetwork,
130	/// Different parts of the payment instructions were inconsistent.
131	///
132	/// A developer-readable error string is provided, though you may or may not wish to provide
133	/// this directly to users.
134	InconsistentInstructions(&'static str),
135	/// The instructions were invalid due to a semantic error.
136	///
137	/// A developer-readable error string is provided, though you may or may not wish to provide
138	/// this directly to users.
139	InvalidInstructions(&'static str),
140	/// The payment instructions did not appear to match any known form of payment instructions.
141	UnknownPaymentInstructions,
142	/// The BIP 321 bitcoin: URI included unknown required parameter(s)
143	UnknownRequiredParameter,
144	/// The call to [`HrnResolver::resolve_hrn`] failed with the contained error.
145	HrnResolutionError(&'static str),
146	// TODO: expiry and check it for ln stuff!
147}
148
149impl PaymentInstructions {
150	/// The maximum amount any payment instruction requires payment for.
151	///
152	/// If `None`, any amount can be paid.
153	///
154	/// Note that we may allow different [`Self::methods`] to have slightly different amounts (e.g.
155	/// if a recipient wishes to be paid more for on-chain payments to offset their future fees),
156	/// but only up to [`MAX_AMOUNT_DIFFERENCE`].
157	pub fn max_amount(&self) -> Option<Amount> {
158		let mut max_amt = None;
159		for method in self.methods() {
160			if let Some(amt) = method.amount() {
161				if max_amt.is_none() || max_amt.unwrap() < amt {
162					max_amt = Some(amt);
163				}
164			}
165		}
166		max_amt
167	}
168
169	/// The amount which the payment instruction requires payment for when paid over lightning.
170	///
171	/// We require that all lightning payment methods in payment instructions require an identical
172	/// amount for payment, and thus if this method returns `None` it indicates either:
173	///  * no lightning payment instructions exist,
174	///  * there is no required amount and any amount can be paid
175	///  * the only lightning payment instructions are for a BOLT 12 offer denominated in a
176	///    non-Bitcoin currency.
177	pub fn ln_payment_amount(&self) -> Option<Amount> {
178		for method in self.methods() {
179			match method {
180				PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
181					return method.amount();
182				},
183				PaymentMethod::OnChain { .. } => {},
184			}
185		}
186		None
187	}
188
189	/// The amount which the payment instruction requires payment for when paid on-chain.
190	///
191	/// There is no way to encode different payment amounts for multiple on-chain formats
192	/// currently, and as such all on-chain [`PaymentMethod`]s will contain the same
193	/// [`PaymentMethod::amount`].
194	pub fn onchain_payment_amount(&self) -> Option<Amount> {
195		for method in self.methods() {
196			match method {
197				PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {},
198				PaymentMethod::OnChain { .. } => {
199					return method.amount();
200				},
201			}
202		}
203		None
204	}
205
206	/// The list of [`PaymentMethod`]s.
207	pub fn methods(&self) -> &[PaymentMethod] {
208		&self.methods
209	}
210
211	/// A recipient-provided description of the payment instructions.
212	///
213	/// This may be:
214	///  * the `label` or `message` parameter in a BIP 321 bitcoin: URI
215	///  * the `description` field in a lightning BOLT 11 invoice
216	///  * the `description` field in a lightning BOLT 12 offer
217	pub fn recipient_description(&self) -> Option<&str> {
218		self.recipient_description.as_ref().map(|x| &**x)
219	}
220
221	/// Fetches the proof-of-payment callback URI.
222	///
223	/// Once a payment has been completed, the proof-of-payment (hex-encoded payment preimage for a
224	/// lightning BOLT 11 invoice, raw transaction serialized in hex for on-chain payments,
225	/// not-yet-defined for lightning BOLT 12 invoices) must be appended to this URI and the URI
226	/// opened with the default system URI handler.
227	pub fn pop_callback(&self) -> Option<&str> {
228		self.pop_callback.as_ref().map(|x| &**x)
229	}
230
231	/// Fetches the [`HumanReadableName`] which was resolved, if the resolved payment instructions
232	/// were for a Human Readable Name.
233	pub fn human_readable_name(&self) -> &Option<HumanReadableName> {
234		&self.hrn
235	}
236
237	/// Fetches the BIP 353 DNSSEC proof which was used to resolve these payment instructions, if
238	/// they were resolved from a HumanReadable Name using BIP 353.
239	///
240	/// This proof should be included in any PSBT output (as type `PSBT_OUT_DNSSEC_PROOF`)
241	/// generated using these payment instructions.
242	///
243	/// It should also be stored to allow us to later prove that this payment was made to
244	/// [`Self::human_readable_name`].
245	pub fn bip_353_dnssec_proof(&self) -> &Option<Vec<u8>> {
246		&self.hrn_proof
247	}
248}
249
250fn instructions_from_bolt11(
251	invoice: Bolt11Invoice, network: Network,
252) -> Result<(Option<String>, impl Iterator<Item = PaymentMethod>), ParseError> {
253	if invoice.network() != network {
254		return Err(ParseError::WrongNetwork);
255	}
256	// TODO: Extract fallback address
257	if let Bolt11InvoiceDescriptionRef::Direct(desc) = invoice.description() {
258		Ok((
259			Some(desc.as_inner().0.clone()),
260			Some(PaymentMethod::LightningBolt11(invoice)).into_iter(),
261		))
262	} else {
263		Ok((None, Some(PaymentMethod::LightningBolt11(invoice)).into_iter()))
264	}
265}
266
267// What str.split_once() should do...
268fn split_once(haystack: &str, needle: char) -> (&str, Option<&str>) {
269	haystack.split_once(needle).map(|(a, b)| (a, Some(b))).unwrap_or((haystack, None))
270}
271
272fn un_percent_encode(encoded: &str) -> Result<String, ParseError> {
273	let mut res = Vec::with_capacity(encoded.len());
274	let mut iter = encoded.bytes();
275	let err = "A Proof of Payment URI was not properly %-encoded in a BIP 321 bitcoin: URI";
276	while let Some(b) = iter.next() {
277		if b == b'%' {
278			let high = iter.next().ok_or(ParseError::InvalidInstructions(err))? as u8;
279			let low = iter.next().ok_or(ParseError::InvalidInstructions(err))?;
280			if high > b'9' || high < b'0' || low > b'9' || low < b'0' {
281				return Err(ParseError::InvalidInstructions(err));
282			}
283			res.push((high - b'0') << 4 | (low - b'0'));
284		} else {
285			res.push(b);
286		}
287	}
288	String::from_utf8(res).map_err(|_| ParseError::InvalidInstructions(err))
289}
290
291#[test]
292fn test_un_percent_encode() {
293	assert_eq!(un_percent_encode("%20").unwrap(), " ");
294	assert_eq!(un_percent_encode("42%20 ").unwrap(), "42  ");
295	assert!(un_percent_encode("42%2").is_err());
296	assert!(un_percent_encode("42%2a").is_err());
297}
298
299fn parse_resolved_instructions(
300	instructions: &str, network: Network, supports_proof_of_payment_callbacks: bool,
301	hrn: Option<HumanReadableName>, hrn_proof: Option<Vec<u8>>,
302) -> Result<PaymentInstructions, ParseError> {
303	const BTC_URI_PFX_LEN: usize = "bitcoin:".len();
304	const LN_URI_PFX_LEN: usize = "lightning:".len();
305
306	if instructions.len() >= BTC_URI_PFX_LEN
307		&& instructions[..BTC_URI_PFX_LEN].eq_ignore_ascii_case("bitcoin:")
308	{
309		let (body, params) = split_once(&instructions[BTC_URI_PFX_LEN..], '?');
310		let mut methods = Vec::new();
311		let mut recipient_description = None;
312		let mut pop_callback = None;
313		if !body.is_empty() {
314			let addr = Address::from_str(body).map_err(|e| ParseError::InvalidOnChain(e))?;
315			let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
316			methods.push(PaymentMethod::OnChain { amount: None, address });
317		}
318		if let Some(params) = params {
319			for param in params.split('&') {
320				let (k, v) = split_once(param, '=');
321				if k.eq_ignore_ascii_case("bc") || k.eq_ignore_ascii_case("req-bc") {
322					if let Some(address_string) = v {
323						if address_string.len() < 3
324							|| !address_string[..3].eq_ignore_ascii_case("bc1")
325						{
326							// `bc` key-values must only include bech32/bech32m strings with HRP
327							// "bc" (i.e. Segwit addresses).
328							let err = "BIP 321 bitcoin: URI contained a bc instruction which was not a Segwit address (bc1*)";
329							return Err(ParseError::InvalidInstructions(err));
330						}
331						let addr = Address::from_str(address_string)
332							.map_err(|e| ParseError::InvalidOnChain(e))?;
333						let address =
334							addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
335						methods.push(PaymentMethod::OnChain { amount: None, address });
336					} else {
337						let err = "BIP 321 bitcoin: URI contained a bc (Segwit address) instruction without a value";
338						return Err(ParseError::InvalidInstructions(err));
339					}
340				} else if k.eq_ignore_ascii_case("lightning")
341					|| k.eq_ignore_ascii_case("req-lightning")
342				{
343					if let Some(invoice_string) = v {
344						let invoice = Bolt11Invoice::from_str(invoice_string)
345							.map_err(|e| ParseError::InvalidBolt11(e))?;
346						let (desc, method_iter) = instructions_from_bolt11(invoice, network)?;
347						if let Some(desc) = desc {
348							recipient_description = Some(desc);
349						}
350						for method in method_iter {
351							methods.push(method);
352						}
353					} else {
354						let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
355						return Err(ParseError::InvalidInstructions(err));
356					}
357				} else if k.eq_ignore_ascii_case("lno") || k.eq_ignore_ascii_case("req-lno") {
358					if let Some(offer_string) = v {
359						let offer = Offer::from_str(offer_string)
360							.map_err(|e| ParseError::InvalidBolt12(e))?;
361						if !offer.supports_chain(network.chain_hash()) {
362							return Err(ParseError::WrongNetwork);
363						}
364						if let Some(desc) = offer.description() {
365							recipient_description = Some(desc.0.to_owned());
366						}
367						methods.push(PaymentMethod::LightningBolt12(offer));
368					} else {
369						let err = "BIP 321 bitcoin: URI contained a lightning (BOLT 11 invoice) instruction without a value";
370						return Err(ParseError::InvalidInstructions(err));
371					}
372				} else if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
373					// We handle this in the second loop below
374				} else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
375					// We handle this in the second loop below
376				} else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
377				{
378					// We handle this in the second loop below
379				} else if k.eq_ignore_ascii_case("pop") || k.eq_ignore_ascii_case("req-pop") {
380					if k.eq_ignore_ascii_case("req-pop") && !supports_proof_of_payment_callbacks {
381						return Err(ParseError::UnknownRequiredParameter);
382					}
383					if let Some(v) = v {
384						let callback_uri = un_percent_encode(v)?;
385						let (proto, _) = split_once(&callback_uri, ':');
386						let proto_isnt_local_app = proto.eq_ignore_ascii_case("javascript")
387							|| proto.eq_ignore_ascii_case("http")
388							|| proto.eq_ignore_ascii_case("https")
389							|| proto.eq_ignore_ascii_case("file")
390							|| proto.eq_ignore_ascii_case("mailto")
391							|| proto.eq_ignore_ascii_case("ftp")
392							|| proto.eq_ignore_ascii_case("wss")
393							|| proto.eq_ignore_ascii_case("ws")
394							|| proto.eq_ignore_ascii_case("ssh")
395							|| proto.eq_ignore_ascii_case("tel")
396							|| proto.eq_ignore_ascii_case("data")
397							|| proto.eq_ignore_ascii_case("blob");
398						if proto_isnt_local_app {
399							let err = "Proof of payment callback would not have opened a local app";
400							return Err(ParseError::InvalidInstructions(err));
401						}
402						pop_callback = Some(callback_uri);
403					} else {
404						let err = "Missing value for a Proof of Payment instruction in a BIP 321 bitcoin: URI";
405						return Err(ParseError::InvalidInstructions(err));
406					}
407				} else if k.len() >= 4 && k[..4].eq_ignore_ascii_case("req-") {
408					return Err(ParseError::UnknownRequiredParameter);
409				}
410			}
411			let mut amount = None;
412			let mut label = None;
413			let mut message = None;
414			for param in params.split('&') {
415				let (k, v) = split_once(param, '=');
416				if k.eq_ignore_ascii_case("amount") || k.eq_ignore_ascii_case("req-amount") {
417					if let Some(v) = v {
418						if amount.is_some() {
419							let err = "Multiple amount parameters in a BIP 321 bitcoin: URI";
420							return Err(ParseError::InvalidInstructions(err));
421						}
422						let err = "The amount parameter in a BIP 321 bitcoin: URI was invalid";
423						let btc_amt =
424							bitcoin::Amount::from_str_in(v, bitcoin::Denomination::Bitcoin)
425								.map_err(|_| ParseError::InvalidInstructions(err))?;
426						amount = Some(Amount::from_sats(btc_amt.to_sat()));
427					} else {
428						let err = "Missing value for an amount parameter in a BIP 321 bitcoin: URI";
429						return Err(ParseError::InvalidInstructions(err));
430					}
431				} else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
432					if label.is_some() {
433						let err = "Multiple label parameters in a BIP 321 bitcoin: URI";
434						return Err(ParseError::InvalidInstructions(err));
435					}
436					label = v;
437				} else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
438				{
439					if message.is_some() {
440						let err = "Multiple message parameters in a BIP 321 bitcoin: URI";
441						return Err(ParseError::InvalidInstructions(err));
442					}
443					message = v;
444				}
445			}
446			// Apply the amount parameter to all on-chain addresses
447			if let Some(uri_amount) = amount {
448				for method in methods.iter_mut() {
449					if let PaymentMethod::OnChain { ref mut amount, .. } = method {
450						*amount = Some(uri_amount);
451					}
452				}
453			}
454
455			if methods.is_empty() {
456				return Err(ParseError::UnknownPaymentInstructions);
457			}
458
459			const MAX_MSATS: u64 = 21_000_000_0000_0000_000;
460			let mut min_amt_msat = MAX_MSATS;
461			let mut max_amt_msat = 0;
462			let mut ln_amt_msat = None;
463			let mut have_amountless_method = false;
464			for method in methods.iter() {
465				if let Some(amt_msat) = method.amount().map(|amt| amt.msats()) {
466					if amt_msat > MAX_MSATS {
467						let err = "Had a payment method in a BIP 321 bitcoin: URI which requested more than 21 million BTC";
468						return Err(ParseError::InvalidInstructions(err));
469					}
470					if amt_msat < min_amt_msat {
471						min_amt_msat = amt_msat;
472					}
473					if amt_msat > max_amt_msat {
474						max_amt_msat = amt_msat;
475					}
476					match method {
477						PaymentMethod::LightningBolt11(_) | PaymentMethod::LightningBolt12(_) => {
478							if let Some(ln_amt_msat) = ln_amt_msat {
479								if ln_amt_msat != amt_msat {
480									let err = "Had multiple different amounts in lightning payment methods in a BIP 321 bitcoin: URI";
481									return Err(ParseError::InconsistentInstructions(err));
482								}
483							}
484							ln_amt_msat = Some(amt_msat);
485						},
486						PaymentMethod::OnChain { .. } => {},
487					}
488				} else {
489					have_amountless_method = true;
490				}
491			}
492			if (min_amt_msat != MAX_MSATS || max_amt_msat != 0) && have_amountless_method {
493				let err = "Had some payment methods in a BIP 321 bitcoin: URI with required amounts, some without";
494				return Err(ParseError::InconsistentInstructions(err));
495			}
496			if max_amt_msat.saturating_sub(min_amt_msat) > MAX_AMOUNT_DIFFERENCE.msats() {
497				let err = "Payment methods differed in ";
498				return Err(ParseError::InconsistentInstructions(err));
499			}
500		}
501		if methods.is_empty() {
502			Err(ParseError::UnknownPaymentInstructions)
503		} else {
504			Ok(PaymentInstructions { recipient_description, methods, pop_callback, hrn, hrn_proof })
505		}
506	} else if instructions.len() >= LN_URI_PFX_LEN
507		&& instructions[..LN_URI_PFX_LEN].eq_ignore_ascii_case("lightning:")
508	{
509		// Though there is no specification, lightning: URIs generally only include BOLT 11
510		// invoices.
511		let invoice = Bolt11Invoice::from_str(&instructions[LN_URI_PFX_LEN..])
512			.map_err(|e| ParseError::InvalidBolt11(e))?;
513		let (recipient_description, method_iter) = instructions_from_bolt11(invoice, network)?;
514		Ok(PaymentInstructions {
515			recipient_description,
516			methods: method_iter.collect(),
517			pop_callback: None,
518			hrn,
519			hrn_proof,
520		})
521	} else {
522		if let Ok(addr) = Address::from_str(instructions) {
523			let address = addr.require_network(network).map_err(|_| ParseError::WrongNetwork)?;
524			Ok(PaymentInstructions {
525				recipient_description: None,
526				methods: vec![PaymentMethod::OnChain { amount: None, address }],
527				pop_callback: None,
528				hrn,
529				hrn_proof,
530			})
531		} else if let Ok(invoice) = Bolt11Invoice::from_str(instructions) {
532			let (recipient_description, method_iter) = instructions_from_bolt11(invoice, network)?;
533			Ok(PaymentInstructions {
534				recipient_description,
535				methods: method_iter.collect(),
536				pop_callback: None,
537				hrn,
538				hrn_proof,
539			})
540		} else if let Ok(offer) = Offer::from_str(instructions) {
541			if !offer.supports_chain(network.chain_hash()) {
542				return Err(ParseError::WrongNetwork);
543			}
544			Ok(PaymentInstructions {
545				recipient_description: offer.description().map(|s| s.0.to_owned()),
546				methods: vec![PaymentMethod::LightningBolt12(offer)],
547				pop_callback: None,
548				hrn,
549				hrn_proof,
550			})
551		} else {
552			Err(ParseError::UnknownPaymentInstructions)
553		}
554	}
555}
556
557/// The resolution of a Human Readable Name
558pub struct HrnResolution {
559	/// A DNSSEC proof as used in BIP 353.
560	///
561	/// If the HRN was resolved using BIP 353, this should be set to a full proof which can later
562	/// be copied to PSBTs for hardware wallet verification or stored as a part of proof of
563	/// payment.
564	pub proof: Option<Vec<u8>>,
565	/// The result of the resolution.
566	///
567	/// This should contain a string which can be parsed as further payment instructions. For a BIP
568	/// 353 resolution, this will contain a full BIP 321 bitcoin: URI, for a LN-Address resolution
569	/// this will contain a lightning BOLT 11 invoice.
570	pub result: String,
571}
572
573/// A future which resolves to a [`HrnResolution`].
574pub type HrnResolutionFuture<'a> =
575	Pin<Box<dyn Future<Output = Result<HrnResolution, &'static str>> + Send + 'a>>;
576
577/// An arbitrary resolver for a Human Readable Name.
578///
579/// In general, such a resolver should first attempt to resolve using DNSSEC as defined in BIP 353.
580///
581/// For clients that also support LN-Address, if the BIP 353 resolution fails they should then fall
582/// back to LN-Address to resolve to a Lightning BOLT 11 using HTTP.
583///
584/// A resolver which uses onion messages over the lightning network for LDK users is provided in
585#[cfg_attr(feature = "std", doc = "[`onion_message_resolver::LDKOnionMessageDNSSECHrnResolver`]")]
586#[cfg_attr(
587	not(feature = "std"),
588	doc = "`onion_message_resolver::LDKOnionMessageDNSSECHrnResolver`"
589)]
590/// if this crate is built with the `std` feature.
591///
592/// A resolver which uses HTTPS to `dns.google` and HTTPS to arbitrary servers for LN-Address is
593/// provided in
594#[cfg_attr(feature = "http", doc = "[`http_resolver::HTTPHrnResolver`]")]
595#[cfg_attr(not(feature = "http"), doc = "`http_resolver::HTTPHrnResolver`")]
596/// if this crate is built with the `http` feature.
597pub trait HrnResolver {
598	/// Resolves the given Human Readable Name into a [`HrnResolution`] containing a result which
599	/// can be further parsed as payment instructions.
600	fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a>;
601}
602
603impl PaymentInstructions {
604	/// Resolves a string into [`PaymentInstructions`].
605	pub async fn parse_payment_instructions<H: HrnResolver>(
606		instructions: &str, network: Network, hrn_resolver: H,
607		supports_proof_of_payment_callbacks: bool,
608	) -> Result<PaymentInstructions, ParseError> {
609		let supports_pops = supports_proof_of_payment_callbacks;
610		if let Ok(hrn) = HumanReadableName::from_encoded(instructions) {
611			let resolution = hrn_resolver.resolve_hrn(&hrn).await;
612			let resolution = resolution.map_err(|e| ParseError::HrnResolutionError(e))?;
613			let res = &resolution.result;
614			parse_resolved_instructions(res, network, supports_pops, Some(hrn), resolution.proof)
615		} else {
616			parse_resolved_instructions(instructions, network, supports_pops, None, None)
617		}
618	}
619}