Skip to content

Commit 45eb0f3

Browse files
authored
Merge pull request #1908 from jkczyz/2022-11-refund
BOLT 12 refund encoding and building
2 parents 56afbf5 + 8e36737 commit 45eb0f3

File tree

6 files changed

+1035
-60
lines changed

6 files changed

+1035
-60
lines changed

lightning/src/offers/invoice_request.rs

+44-35
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99

1010
//! Data structures and encoding for `invoice_request` messages.
1111
//!
12-
//! An [`InvoiceRequest`] can be either built from a parsed [`Offer`] as an "offer to be paid" or
13-
//! built directly as an "offer for money" (e.g., refund, ATM withdrawal). In the former case, it is
12+
//! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
1413
//! typically constructed by a customer and sent to the merchant who had published the corresponding
15-
//! offer. In the latter case, an offer doesn't exist as a precursor to the request. Rather the
16-
//! merchant would typically construct the invoice request and present it to the customer.
14+
//! offer. The recipient of the request responds with an `Invoice`.
1715
//!
18-
//! The recipient of the request responds with an `Invoice`.
16+
//! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
17+
//! precursor, see [`Refund`].
18+
//!
19+
//! [`Refund`]: crate::offers::refund::Refund
1920
//!
2021
//! ```ignore
2122
//! extern crate bitcoin;
@@ -34,7 +35,6 @@
3435
//! let pubkey = PublicKey::from(keys);
3536
//! let mut buffer = Vec::new();
3637
//!
37-
//! // "offer to be paid" flow
3838
//! "lno1qcp4256ypq"
3939
//! .parse::<Offer>()?
4040
//! .request_invoice(vec![42; 64], pubkey)?
@@ -287,7 +287,7 @@ impl InvoiceRequest {
287287
self.contents.amount_msats
288288
}
289289

290-
/// Features for paying the invoice.
290+
/// Features pertaining to requesting an invoice.
291291
pub fn features(&self) -> &InvoiceRequestFeatures {
292292
&self.contents.features
293293
}
@@ -471,7 +471,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
471471

472472
#[cfg(test)]
473473
mod tests {
474-
use super::InvoiceRequest;
474+
use super::{InvoiceRequest, InvoiceRequestTlvStreamRef};
475475

476476
use bitcoin::blockdata::constants::ChainHash;
477477
use bitcoin::network::constants::Network;
@@ -483,9 +483,10 @@ mod tests {
483483
use core::time::Duration;
484484
use crate::ln::features::InvoiceRequestFeatures;
485485
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
486-
use crate::offers::merkle::SignError;
487-
use crate::offers::offer::{Amount, OfferBuilder, Quantity};
486+
use crate::offers::merkle::{SignError, SignatureTlvStreamRef};
487+
use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
488488
use crate::offers::parse::{ParseError, SemanticError};
489+
use crate::offers::payer::PayerTlvStreamRef;
489490
use crate::util::ser::{BigSize, Writeable};
490491
use crate::util::string::PrintableString;
491492

@@ -517,14 +518,13 @@ mod tests {
517518

518519
#[test]
519520
fn builds_invoice_request_with_defaults() {
520-
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
521+
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
521522
.amount_msats(1000)
522-
.build().unwrap();
523-
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
524-
.build().unwrap().sign(payer_sign).unwrap();
523+
.build().unwrap()
524+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
525+
.build().unwrap()
526+
.sign(payer_sign).unwrap();
525527

526-
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream) =
527-
invoice_request.as_tlv_stream();
528528
let mut buffer = Vec::new();
529529
invoice_request.write(&mut buffer).unwrap();
530530

@@ -538,25 +538,34 @@ mod tests {
538538
assert_eq!(invoice_request.payer_note(), None);
539539
assert!(invoice_request.signature().is_some());
540540

541-
assert_eq!(payer_tlv_stream.metadata, Some(&vec![1; 32]));
542-
assert_eq!(offer_tlv_stream.chains, None);
543-
assert_eq!(offer_tlv_stream.metadata, None);
544-
assert_eq!(offer_tlv_stream.currency, None);
545-
assert_eq!(offer_tlv_stream.amount, Some(1000));
546-
assert_eq!(offer_tlv_stream.description, Some(&String::from("foo")));
547-
assert_eq!(offer_tlv_stream.features, None);
548-
assert_eq!(offer_tlv_stream.absolute_expiry, None);
549-
assert_eq!(offer_tlv_stream.paths, None);
550-
assert_eq!(offer_tlv_stream.issuer, None);
551-
assert_eq!(offer_tlv_stream.quantity_max, None);
552-
assert_eq!(offer_tlv_stream.node_id, Some(&recipient_pubkey()));
553-
assert_eq!(invoice_request_tlv_stream.chain, None);
554-
assert_eq!(invoice_request_tlv_stream.amount, None);
555-
assert_eq!(invoice_request_tlv_stream.features, None);
556-
assert_eq!(invoice_request_tlv_stream.quantity, None);
557-
assert_eq!(invoice_request_tlv_stream.payer_id, Some(&payer_pubkey()));
558-
assert_eq!(invoice_request_tlv_stream.payer_note, None);
559-
assert!(signature_tlv_stream.signature.is_some());
541+
assert_eq!(
542+
invoice_request.as_tlv_stream(),
543+
(
544+
PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
545+
OfferTlvStreamRef {
546+
chains: None,
547+
metadata: None,
548+
currency: None,
549+
amount: Some(1000),
550+
description: Some(&String::from("foo")),
551+
features: None,
552+
absolute_expiry: None,
553+
paths: None,
554+
issuer: None,
555+
quantity_max: None,
556+
node_id: Some(&recipient_pubkey()),
557+
},
558+
InvoiceRequestTlvStreamRef {
559+
chain: None,
560+
amount: None,
561+
features: None,
562+
quantity: None,
563+
payer_id: Some(&payer_pubkey()),
564+
payer_note: None,
565+
},
566+
SignatureTlvStreamRef { signature: invoice_request.signature().as_ref() },
567+
),
568+
);
560569

561570
if let Err(e) = InvoiceRequest::try_from(buffer) {
562571
panic!("error parsing invoice request: {:?}", e);

lightning/src/offers/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ mod merkle;
1717
pub mod offer;
1818
pub mod parse;
1919
mod payer;
20+
pub mod refund;

lightning/src/offers/offer.rs

+33-25
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ impl OfferBuilder {
106106
let offer = OfferContents {
107107
chains: None, metadata: None, amount: None, description,
108108
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
109-
supported_quantity: Quantity::one(), signing_pubkey: Some(signing_pubkey),
109+
supported_quantity: Quantity::one(), signing_pubkey,
110110
};
111111
OfferBuilder { offer }
112112
}
@@ -263,7 +263,7 @@ pub(super) struct OfferContents {
263263
issuer: Option<String>,
264264
paths: Option<Vec<BlindedPath>>,
265265
supported_quantity: Quantity,
266-
signing_pubkey: Option<PublicKey>,
266+
signing_pubkey: PublicKey,
267267
}
268268

269269
impl Offer {
@@ -359,7 +359,7 @@ impl Offer {
359359

360360
/// The public key used by the recipient to sign invoices.
361361
pub fn signing_pubkey(&self) -> PublicKey {
362-
self.contents.signing_pubkey.unwrap()
362+
self.contents.signing_pubkey
363363
}
364364

365365
/// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
@@ -497,7 +497,7 @@ impl OfferContents {
497497
paths: self.paths.as_ref(),
498498
issuer: self.issuer.as_ref(),
499499
quantity_max: self.supported_quantity.to_tlv_record(),
500-
node_id: self.signing_pubkey.as_ref(),
500+
node_id: Some(&self.signing_pubkey),
501501
}
502502
}
503503
}
@@ -634,13 +634,14 @@ impl TryFrom<OfferTlvStream> for OfferContents {
634634
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
635635
};
636636

637-
if node_id.is_none() {
638-
return Err(SemanticError::MissingSigningPubkey);
639-
}
637+
let signing_pubkey = match node_id {
638+
None => return Err(SemanticError::MissingSigningPubkey),
639+
Some(node_id) => node_id,
640+
};
640641

641642
Ok(OfferContents {
642643
chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
643-
supported_quantity, signing_pubkey: node_id,
644+
supported_quantity, signing_pubkey,
644645
})
645646
}
646647
}
@@ -653,7 +654,7 @@ impl core::fmt::Display for Offer {
653654

654655
#[cfg(test)]
655656
mod tests {
656-
use super::{Amount, Offer, OfferBuilder, Quantity};
657+
use super::{Amount, Offer, OfferBuilder, OfferTlvStreamRef, Quantity};
657658

658659
use bitcoin::blockdata::constants::ChainHash;
659660
use bitcoin::network::constants::Network;
@@ -680,7 +681,7 @@ mod tests {
680681
#[test]
681682
fn builds_offer_with_defaults() {
682683
let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
683-
let tlv_stream = offer.as_tlv_stream();
684+
684685
let mut buffer = Vec::new();
685686
offer.write(&mut buffer).unwrap();
686687

@@ -699,17 +700,22 @@ mod tests {
699700
assert_eq!(offer.supported_quantity(), Quantity::one());
700701
assert_eq!(offer.signing_pubkey(), pubkey(42));
701702

702-
assert_eq!(tlv_stream.chains, None);
703-
assert_eq!(tlv_stream.metadata, None);
704-
assert_eq!(tlv_stream.currency, None);
705-
assert_eq!(tlv_stream.amount, None);
706-
assert_eq!(tlv_stream.description, Some(&String::from("foo")));
707-
assert_eq!(tlv_stream.features, None);
708-
assert_eq!(tlv_stream.absolute_expiry, None);
709-
assert_eq!(tlv_stream.paths, None);
710-
assert_eq!(tlv_stream.issuer, None);
711-
assert_eq!(tlv_stream.quantity_max, None);
712-
assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
703+
assert_eq!(
704+
offer.as_tlv_stream(),
705+
OfferTlvStreamRef {
706+
chains: None,
707+
metadata: None,
708+
currency: None,
709+
amount: None,
710+
description: Some(&String::from("foo")),
711+
features: None,
712+
absolute_expiry: None,
713+
paths: None,
714+
issuer: None,
715+
quantity_max: None,
716+
node_id: Some(&pubkey(42)),
717+
},
718+
);
713719

714720
if let Err(e) = Offer::try_from(buffer) {
715721
panic!("error parsing offer: {:?}", e);
@@ -1121,11 +1127,13 @@ mod tests {
11211127
panic!("error parsing offer: {:?}", e);
11221128
}
11231129

1124-
let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
1125-
builder.offer.signing_pubkey = None;
1130+
let mut tlv_stream = offer.as_tlv_stream();
1131+
tlv_stream.node_id = None;
11261132

1127-
let offer = builder.build().unwrap();
1128-
match offer.to_string().parse::<Offer>() {
1133+
let mut encoded_offer = Vec::new();
1134+
tlv_stream.write(&mut encoded_offer).unwrap();
1135+
1136+
match Offer::try_from(encoded_offer) {
11291137
Ok(_) => panic!("expected error"),
11301138
Err(e) => {
11311139
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));

lightning/src/offers/parse.rs

+8
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,28 @@ pub enum SemanticError {
127127
AlreadyExpired,
128128
/// The provided chain hash does not correspond to a supported chain.
129129
UnsupportedChain,
130+
/// A chain was provided but was not expected.
131+
UnexpectedChain,
130132
/// An amount was expected but was missing.
131133
MissingAmount,
132134
/// The amount exceeded the total bitcoin supply.
133135
InvalidAmount,
134136
/// An amount was provided but was not sufficient in value.
135137
InsufficientAmount,
138+
/// An amount was provided but was not expected.
139+
UnexpectedAmount,
136140
/// A currency was provided that is not supported.
137141
UnsupportedCurrency,
138142
/// A feature was required but is unknown.
139143
UnknownRequiredFeatures,
144+
/// Features were provided but were not expected.
145+
UnexpectedFeatures,
140146
/// A required description was not provided.
141147
MissingDescription,
142148
/// A signing pubkey was not provided.
143149
MissingSigningPubkey,
150+
/// A signing pubkey was provided but was not expected.
151+
UnexpectedSigningPubkey,
144152
/// A quantity was expected but was missing.
145153
MissingQuantity,
146154
/// An unsupported quantity was provided.

0 commit comments

Comments
 (0)