@@ -135,7 +135,7 @@ use crate::offers::merkle::{
135135} ;
136136use crate :: offers:: nonce:: Nonce ;
137137use crate :: offers:: offer:: {
138- Amount , ExperimentalOfferTlvStream , ExperimentalOfferTlvStreamRef , OfferTlvStream ,
138+ Amount , ExperimentalOfferTlvStream , ExperimentalOfferTlvStreamRef , OfferId , OfferTlvStream ,
139139 OfferTlvStreamRef , Quantity , EXPERIMENTAL_OFFER_TYPES , OFFER_TYPES ,
140140} ;
141141use crate :: offers:: parse:: { Bolt12ParseError , Bolt12SemanticError , ParsedMessage } ;
@@ -665,6 +665,21 @@ impl UnsignedBolt12Invoice {
665665 pub fn tagged_hash ( & self ) -> & TaggedHash {
666666 & self . tagged_hash
667667 }
668+
669+ /// Computes the offer ID if this invoice corresponds to an offer.
670+ fn compute_offer_id ( & self ) -> Option < OfferId > {
671+ match & self . contents {
672+ InvoiceContents :: ForOffer { .. } => {
673+ // Extract offer TLV records from the invoice bytes
674+ let offer_tlv_stream = TlvStream :: new ( & self . bytes ) . range ( OFFER_TYPES ) ;
675+ let experimental_offer_tlv_stream = TlvStream :: new ( & self . experimental_bytes ) . range ( EXPERIMENTAL_OFFER_TYPES ) ;
676+ let combined_tlv_stream = offer_tlv_stream. chain ( experimental_offer_tlv_stream) ;
677+ let tagged_hash = TaggedHash :: from_tlv_stream ( "LDK Offer ID" , combined_tlv_stream) ;
678+ Some ( OfferId ( tagged_hash. to_bytes ( ) ) )
679+ } ,
680+ InvoiceContents :: ForRefund { .. } => None ,
681+ }
682+ }
668683}
669684
670685macro_rules! unsigned_invoice_sign_method { ( $self: ident, $self_type: ty $( , $self_mut: tt) ?) => {
@@ -686,6 +701,9 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
686701 // Append the experimental bytes after the signature.
687702 $self. bytes. extend_from_slice( & $self. experimental_bytes) ;
688703
704+ // Compute offer_id before moving fields
705+ let offer_id = $self. compute_offer_id( ) ;
706+
689707 Ok ( Bolt12Invoice {
690708 #[ cfg( not( c_bindings) ) ]
691709 bytes: $self. bytes,
@@ -700,6 +718,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
700718 tagged_hash: $self. tagged_hash,
701719 #[ cfg( c_bindings) ]
702720 tagged_hash: $self. tagged_hash. clone( ) ,
721+ offer_id,
703722 } )
704723 }
705724} }
@@ -734,6 +753,7 @@ pub struct Bolt12Invoice {
734753 contents : InvoiceContents ,
735754 signature : Signature ,
736755 tagged_hash : TaggedHash ,
756+ offer_id : Option < OfferId > ,
737757}
738758
739759/// The contents of an [`Bolt12Invoice`] for responding to either an [`Offer`] or a [`Refund`].
@@ -967,6 +987,11 @@ impl Bolt12Invoice {
967987 self . tagged_hash . as_digest ( ) . as_ref ( ) . clone ( )
968988 }
969989
990+ /// Returns the offer ID if this invoice corresponds to an offer.
991+ pub fn offer_id ( & self ) -> Option < OfferId > {
992+ self . offer_id
993+ }
994+
970995 /// Verifies that the invoice was for a request or refund created using the given key by
971996 /// checking the payer metadata from the invoice request.
972997 ///
@@ -1622,7 +1647,7 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
16221647 let pubkey = contents. fields ( ) . signing_pubkey ;
16231648 merkle:: verify_signature ( & signature, & tagged_hash, pubkey) ?;
16241649
1625- Ok ( Bolt12Invoice { bytes, contents, signature, tagged_hash } )
1650+ Ok ( Bolt12Invoice { bytes, contents, signature, tagged_hash, offer_id : None } )
16261651 }
16271652}
16281653
@@ -3556,4 +3581,62 @@ mod tests {
35563581 ) ,
35573582 }
35583583 }
3584+
3585+ #[ test]
3586+ fn invoice_offer_id_matches_offer_id ( ) {
3587+ let expanded_key = ExpandedKey :: new ( [ 42 ; 32 ] ) ;
3588+ let entropy = FixedEntropy { } ;
3589+ let nonce = Nonce :: from_entropy_source ( & entropy) ;
3590+ let secp_ctx = Secp256k1 :: new ( ) ;
3591+ let payment_id = PaymentId ( [ 1 ; 32 ] ) ;
3592+
3593+ // Create an offer
3594+ let offer = OfferBuilder :: new ( recipient_pubkey ( ) )
3595+ . amount_msats ( 1000 )
3596+ . build ( )
3597+ . unwrap ( ) ;
3598+
3599+ // Get the offer ID
3600+ let offer_id = offer. id ( ) ;
3601+
3602+ // Create an invoice request from the offer
3603+ let invoice_request = offer
3604+ . request_invoice ( & expanded_key, nonce, & secp_ctx, payment_id)
3605+ . unwrap ( )
3606+ . build_and_sign ( )
3607+ . unwrap ( ) ;
3608+
3609+ // Create an invoice from the invoice request
3610+ let invoice = invoice_request
3611+ . respond_with_no_std ( payment_paths ( ) , payment_hash ( ) , now ( ) )
3612+ . unwrap ( )
3613+ . build ( )
3614+ . unwrap ( )
3615+ . sign ( recipient_sign)
3616+ . unwrap ( ) ;
3617+
3618+ // Verify that the invoice's offer_id matches the offer's id
3619+ assert_eq ! ( invoice. offer_id( ) , Some ( offer_id) ) ;
3620+ }
3621+
3622+ #[ test]
3623+ fn refund_invoice_has_no_offer_id ( ) {
3624+ // Create a refund
3625+ let refund = RefundBuilder :: new ( vec ! [ 1 ; 32 ] , payer_pubkey ( ) , 1000 )
3626+ . unwrap ( )
3627+ . build ( )
3628+ . unwrap ( ) ;
3629+
3630+ // Create an invoice from the refund
3631+ let invoice = refund
3632+ . respond_with_no_std ( payment_paths ( ) , payment_hash ( ) , recipient_pubkey ( ) , now ( ) )
3633+ . unwrap ( )
3634+ . build ( )
3635+ . unwrap ( )
3636+ . sign ( recipient_sign)
3637+ . unwrap ( ) ;
3638+
3639+ // Verify that the refund invoice has no offer_id
3640+ assert_eq ! ( invoice. offer_id( ) , None ) ;
3641+ }
35593642}
0 commit comments