From f779bc066fe9a0da409502cd0c4655ba71c50a93 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 20 Dec 2022 09:33:11 -0600 Subject: [PATCH] Builder for creating invoices for refunds 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 | 49 +++++++++++++++++++++++---------- lightning/src/offers/refund.rs | 32 +++++++++++++++++++-- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index a937a5483..24bf160e5 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -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 { - 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 { + 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, contents: InvoiceContents) -> Result { + 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 diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index fdf763ecb..e5d8e78f0 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -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, - contents: RefundContents, + pub(super) bytes: Vec, + 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 { + 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() -- 2.39.5