bitcoin_payment_instructions/
lib.rs1#![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#[derive(PartialEq, Eq, Debug)]
50pub enum PaymentMethod {
51 LightningBolt11(Bolt11Invoice),
53 LightningBolt12(Offer),
55 OnChain {
57 amount: Option<Amount>,
63 address: Address,
65 },
66}
67
68impl PaymentMethod {
69 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#[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
112pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats(100);
118
119#[derive(Debug)]
121pub enum ParseError {
122 InvalidBolt11(ParseOrSemanticError),
124 InvalidBolt12(Bolt12ParseError),
126 InvalidOnChain(address::ParseError),
128 WrongNetwork,
130 InconsistentInstructions(&'static str),
135 InvalidInstructions(&'static str),
140 UnknownPaymentInstructions,
142 UnknownRequiredParameter,
144 HrnResolutionError(&'static str),
146 }
148
149impl PaymentInstructions {
150 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 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 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 pub fn methods(&self) -> &[PaymentMethod] {
208 &self.methods
209 }
210
211 pub fn recipient_description(&self) -> Option<&str> {
218 self.recipient_description.as_ref().map(|x| &**x)
219 }
220
221 pub fn pop_callback(&self) -> Option<&str> {
228 self.pop_callback.as_ref().map(|x| &**x)
229 }
230
231 pub fn human_readable_name(&self) -> &Option<HumanReadableName> {
234 &self.hrn
235 }
236
237 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 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
267fn 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 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 } else if k.eq_ignore_ascii_case("label") || k.eq_ignore_ascii_case("req-label") {
375 } else if k.eq_ignore_ascii_case("message") || k.eq_ignore_ascii_case("req-message")
377 {
378 } 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 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 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
557pub struct HrnResolution {
559 pub proof: Option<Vec<u8>>,
565 pub result: String,
571}
572
573pub type HrnResolutionFuture<'a> =
575 Pin<Box<dyn Future<Output = Result<HrnResolution, &'static str>> + Send + 'a>>;
576
577#[cfg_attr(feature = "std", doc = "[`onion_message_resolver::LDKOnionMessageDNSSECHrnResolver`]")]
586#[cfg_attr(
587 not(feature = "std"),
588 doc = "`onion_message_resolver::LDKOnionMessageDNSSECHrnResolver`"
589)]
590#[cfg_attr(feature = "http", doc = "[`http_resolver::HTTPHrnResolver`]")]
595#[cfg_attr(not(feature = "http"), doc = "`http_resolver::HTTPHrnResolver`")]
596pub trait HrnResolver {
598 fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a>;
601}
602
603impl PaymentInstructions {
604 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}