From ffed552cc6296ef4831cf6dd109f62d3a185f851 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Sat, 24 May 2025 12:01:48 +0200 Subject: [PATCH] offers: parse invoice and invoice request Add the ability to parse and display BOLT12 invoices and invoice requests. This makes it easy for users of LDK to serialize and deserialize BOLT12 invoices and invoice requests. --- lightning/src/offers/invoice.rs | 43 +++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 44 ++++++++++++++++++++++++- lightning/src/offers/merkle.rs | 19 +---------- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 3615850a22e..e1f5c13daf6 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -138,7 +138,7 @@ use crate::offers::offer::{ Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; -use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; +use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef, PAYER_METADATA_TYPE}; use crate::offers::refund::{ Refund, RefundContents, IV_BYTES_WITHOUT_METADATA as REFUND_IV_BYTES_WITHOUT_METADATA, @@ -158,6 +158,7 @@ use bitcoin::secp256k1::schnorr::Signature; use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1}; use bitcoin::{Network, WitnessProgram, WitnessVersion}; use core::hash::{Hash, Hasher}; +use core::str::FromStr; use core::time::Duration; #[allow(unused_imports)] @@ -1416,6 +1417,30 @@ impl Writeable for InvoiceContents { } } +impl AsRef<[u8]> for Bolt12Invoice { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +impl Bech32Encode for Bolt12Invoice { + const BECH32_HRP: &'static str = "lni"; +} + +impl FromStr for Bolt12Invoice { + type Err = Bolt12ParseError; + + fn from_str(s: &str) -> Result::Err> { + Self::from_bech32_str(s) + } +} + +impl core::fmt::Display for Bolt12Invoice { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + impl TryFrom> for UnsignedBolt12Invoice { type Error = Bolt12ParseError; @@ -2572,6 +2597,22 @@ mod tests { } } + #[test] + fn parses_bech32_encoded_invoices() { + let invoices = [ + "lni1qqsg7jpsyzz4hcsj0hu6rvjevwhmkceurq7sd5ez8ne3js4qt8acvxcgqgp7szsqzcss9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jtqss9l7txvy6ukzg8zkxdnvzmg2at4stt004vdqrm0zedsez596nf5w55r7sr3qzhfe2d696205tjuddpjvz8952aaxh3n527f26ks7llqcq8jgzlwxsxhzwphk8y90zdqee8pesuhjst2nz2px6ska9wyr2g666ysz0e8vwqgptkk94lm99qhr5ahqqpkpg9lz4deg6zqj0erna0etvd7y8chydtusq9vqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz3afsfc3h8etwulthfjufa8c6lm8saelrud6h7xyeprcxnk4rd3sqqtqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq46w2nw3wjnazuhrtgvnq3edzh0f4uvazhj2k458hlcxqpujqhm35p4cnsda3eptcngxwfcwv89u5z65cjsfk59hft3q6jxkk3yqn7fmrszqw2gk576jl7lvaxqsae3tt9uepmp4gae5kptgwvc97a04jvljuss7qpdqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0vc5l00vl5rwqgc7cmxyrgtuz8dvv6yma5qs2609uvyfe7wvq2gxqpwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz3rsqqqqqqsqqqraqqz5qqqqqqqqqqqvsqqqq8g6jj3qqqqqqqqqqqpqqqq86qq9gqqqqqqqqqqqeqqqqqw3499zqqqqq9yq35rr8sh4qsz52329g4z52329g4z52329g4z52329g4z52329g4z52329g4z5242qgp73tzaqqqzpcasc3pf3lquzjd0haxgn9hmjfp84eq7geymjdx2f9verdu99wz4qqqpf67qrgen88wz7kzlkpyp480l5rgzecaz2qgqyza43d07efg9ca8dcqqds2p0c4tw2xssyn7gult72mr03p79er2l9vppq2a43d07efg9ca8dcqqds2p0c4tw2xssyn7gult72mr03p79er2l9uzq9wktr4p2qxgmdnpw8qvs05qr0zvam2h52lxt4zz7lah7yp6vmsczevlvqdgjxtwdlp84304uqcygvqcgzpj8p44smqjpzeua0xryrrc" + ]; + for encoded in invoices { + let decoded = match encoded.parse::() { + Ok(decoded) => decoded, + Err(e) => panic!("Invalid invoice ({:?}): {}", e, encoded), + }; + + let reencoded = decoded.to_string(); + assert_eq!(reencoded, encoded, "Re-encoded invoice does not match original"); + } + } + #[test] fn parses_invoice_with_payment_paths() { let expanded_key = ExpandedKey::new([42; 32]); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 2e399738bae..77848d97fe2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -65,6 +65,8 @@ //! # } //! ``` +use core::str::FromStr; + use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::BlindedPaymentPath; use crate::io; @@ -79,7 +81,7 @@ use crate::offers::offer::{ Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; -use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; +use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::dns_resolution::HumanReadableName; @@ -1284,6 +1286,30 @@ impl TryFrom> for UnsignedInvoiceRequest { } } +impl AsRef<[u8]> for InvoiceRequest { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +impl Bech32Encode for InvoiceRequest { + const BECH32_HRP: &'static str = "lnr"; +} + +impl FromStr for InvoiceRequest { + type Err = Bolt12ParseError; + + fn from_str(s: &str) -> Result::Err> { + Self::from_bech32_str(s) + } +} + +impl core::fmt::Display for InvoiceRequest { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + impl TryFrom> for InvoiceRequest { type Error = Bolt12ParseError; @@ -2219,6 +2245,22 @@ mod tests { } } + #[test] + fn parses_bech32_encoded_invoice_requests() { + let invoice_requests = [ + "lnr1qqsg7jpsyzz4hcsj0hu6rvjevwhmkceurq7sd5ez8ne3js4qt8acvxcgqgp7szsqzsqpvggzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6he9yqs86ptqzqjcyypqk4jf95qryjcsqywr6kktzrf366ex4yp8cr5r8m32cre3kfea7w0sgzegrzqgucwd37cjyvkgg2lfae8j6wyyx7dj3aqe8j2ncrthhszl8r69lecma5cxclmft4kh8x39jaeqtdl2yy5gsfdqcpvxczf5x0sw" + ]; + for encoded in invoice_requests { + let decoded = match encoded.parse::() { + Ok(decoded) => decoded, + Err(e) => panic!("Invalid invoice request ({:?}): {}", e, encoded), + }; + + let reencoded = decoded.to_string(); + assert_eq!(reencoded, encoded, "Re-encoded invoice does not match original"); + } + } + #[test] fn parses_invoice_request_with_metadata() { let expanded_key = ExpandedKey::new([42; 32]); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 3c84e7a7a17..67029495bab 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -285,10 +285,9 @@ mod tests { use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::ExpandedKey; - use crate::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest}; + use crate::offers::invoice_request::UnsignedInvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, OfferBuilder}; - use crate::offers::parse::Bech32Encode; use crate::offers::signer::Metadata; use crate::offers::test_utils::recipient_pubkey; use crate::util::ser::Writeable; @@ -477,20 +476,4 @@ mod tests { assert_eq!(tlv_stream, invoice_request.bytes); } - - impl AsRef<[u8]> for InvoiceRequest { - fn as_ref(&self) -> &[u8] { - &self.bytes - } - } - - impl Bech32Encode for InvoiceRequest { - const BECH32_HRP: &'static str = "lnr"; - } - - impl core::fmt::Display for InvoiceRequest { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - self.fmt_bech32_str(f) - } - } }