Builder for creating invoices for refunds
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 20 Dec 2022 15:33:11 +0000 (09:33 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 20 Jan 2023 22:04:38 +0000 (16:04 -0600)
Add a builder for creating invoices for a refund and required fields.
Other settings are optional and duplicative settings will override
previous settings. Building produces a semantically valid `invoice`
message for the refund, which then can be signed with the key associated
with the provided signing pubkey.

lightning/src/offers/invoice.rs
lightning/src/offers/refund.rs

index a937a5483c93cd48b55128b590998174807bad36..24bf160e56c9ad6bb07cdc09c484de04607d1679 100644 (file)
@@ -28,7 +28,7 @@ use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef
 use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
 use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
-use crate::offers::refund::RefundContents;
+use crate::offers::refund::{Refund, RefundContents};
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer};
 
@@ -60,10 +60,6 @@ impl<'a> InvoiceBuilder<'a> {
                invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
                created_at: Duration, payment_hash: PaymentHash
        ) -> Result<Self, SemanticError> {
-               if payment_paths.is_empty() {
-                       return Err(SemanticError::MissingPaths);
-               }
-
                let amount_msats = match invoice_request.amount_msats() {
                        Some(amount_msats) => amount_msats,
                        None => match invoice_request.contents.offer.amount() {
@@ -75,17 +71,40 @@ impl<'a> InvoiceBuilder<'a> {
                        },
                };
 
-               Ok(Self {
-                       invreq_bytes: &invoice_request.bytes,
-                       invoice: InvoiceContents::ForOffer {
-                               invoice_request: invoice_request.contents.clone(),
-                               fields: InvoiceFields {
-                                       payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
-                                       fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
-                                       signing_pubkey: invoice_request.contents.offer.signing_pubkey(),
-                               },
+               let contents = InvoiceContents::ForOffer {
+                       invoice_request: invoice_request.contents.clone(),
+                       fields: InvoiceFields {
+                               payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
+                               fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
+                               signing_pubkey: invoice_request.contents.offer.signing_pubkey(),
                        },
-               })
+               };
+
+               Self::new(&invoice_request.bytes, contents)
+       }
+
+       pub(super) fn for_refund(
+               refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+               payment_hash: PaymentHash, signing_pubkey: PublicKey
+       ) -> Result<Self, SemanticError> {
+               let contents = InvoiceContents::ForRefund {
+                       refund: refund.contents.clone(),
+                       fields: InvoiceFields {
+                               payment_paths, created_at, relative_expiry: None, payment_hash,
+                               amount_msats: refund.amount_msats(), fallbacks: None,
+                               features: Bolt12InvoiceFeatures::empty(), signing_pubkey,
+                       },
+               };
+
+               Self::new(&refund.bytes, contents)
+       }
+
+       fn new(invreq_bytes: &'a Vec<u8>, contents: InvoiceContents) -> Result<Self, SemanticError> {
+               if contents.fields().payment_paths.is_empty() {
+                       return Err(SemanticError::MissingPaths);
+               }
+
+               Ok(Self { invreq_bytes, invoice: contents })
        }
 
        /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
index fdf763ecb952653dfadea965a5978fe5dd8e841e..e5d8e78f0794e08c7dee1fe8e994bacb9ab14cd2 100644 (file)
@@ -78,8 +78,10 @@ use core::convert::TryFrom;
 use core::str::FromStr;
 use core::time::Duration;
 use crate::io;
+use crate::ln::PaymentHash;
 use crate::ln::features::InvoiceRequestFeatures;
 use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
 use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
 use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
@@ -202,8 +204,8 @@ impl RefundBuilder {
 /// [`Offer`]: crate::offers::offer::Offer
 #[derive(Clone, Debug)]
 pub struct Refund {
-       bytes: Vec<u8>,
-       contents: RefundContents,
+       pub(super) bytes: Vec<u8>,
+       pub(super) contents: RefundContents,
 }
 
 /// The contents of a [`Refund`], which may be shared with an [`Invoice`].
@@ -296,6 +298,32 @@ impl Refund {
                self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
        }
 
+       /// Creates an [`Invoice`] for the refund with the given required fields.
+       ///
+       /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
+       /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to
+       /// claim a payment for the invoice.
+       ///
+       /// The `signing_pubkey` is required to sign the invoice since refunds are not in response to an
+       /// offer, which does have a `signing_pubkey`.
+       ///
+       /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
+       /// must contain one or more elements.
+       ///
+       /// Errors if the request contains unknown required features.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       pub fn respond_with(
+               &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+               payment_hash: PaymentHash, signing_pubkey: PublicKey
+       ) -> Result<InvoiceBuilder, SemanticError> {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey)
+       }
+
        #[cfg(test)]
        fn as_tlv_stream(&self) -> RefundTlvStreamRef {
                self.contents.as_tlv_stream()