Skip to content

BOLT 12 refund encoding and building #1908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 44 additions & 35 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

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

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

#[cfg(test)]
mod tests {
use super::InvoiceRequest;
use super::{InvoiceRequest, InvoiceRequestTlvStreamRef};

use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
Expand All @@ -483,9 +483,10 @@ mod tests {
use core::time::Duration;
use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
use crate::offers::merkle::SignError;
use crate::offers::offer::{Amount, OfferBuilder, Quantity};
use crate::offers::merkle::{SignError, SignatureTlvStreamRef};
use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
use crate::offers::parse::{ParseError, SemanticError};
use crate::offers::payer::PayerTlvStreamRef;
use crate::util::ser::{BigSize, Writeable};
use crate::util::string::PrintableString;

Expand Down Expand Up @@ -517,14 +518,13 @@ mod tests {

#[test]
fn builds_invoice_request_with_defaults() {
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.build().unwrap();
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap().sign(payer_sign).unwrap();
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap();

let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream) =
invoice_request.as_tlv_stream();
let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

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

assert_eq!(payer_tlv_stream.metadata, Some(&vec![1; 32]));
assert_eq!(offer_tlv_stream.chains, None);
assert_eq!(offer_tlv_stream.metadata, None);
assert_eq!(offer_tlv_stream.currency, None);
assert_eq!(offer_tlv_stream.amount, Some(1000));
assert_eq!(offer_tlv_stream.description, Some(&String::from("foo")));
assert_eq!(offer_tlv_stream.features, None);
assert_eq!(offer_tlv_stream.absolute_expiry, None);
assert_eq!(offer_tlv_stream.paths, None);
assert_eq!(offer_tlv_stream.issuer, None);
assert_eq!(offer_tlv_stream.quantity_max, None);
assert_eq!(offer_tlv_stream.node_id, Some(&recipient_pubkey()));
assert_eq!(invoice_request_tlv_stream.chain, None);
assert_eq!(invoice_request_tlv_stream.amount, None);
assert_eq!(invoice_request_tlv_stream.features, None);
assert_eq!(invoice_request_tlv_stream.quantity, None);
assert_eq!(invoice_request_tlv_stream.payer_id, Some(&payer_pubkey()));
assert_eq!(invoice_request_tlv_stream.payer_note, None);
assert!(signature_tlv_stream.signature.is_some());
assert_eq!(
invoice_request.as_tlv_stream(),
(
PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
OfferTlvStreamRef {
chains: None,
metadata: None,
currency: None,
amount: Some(1000),
description: Some(&String::from("foo")),
features: None,
absolute_expiry: None,
paths: None,
issuer: None,
quantity_max: None,
node_id: Some(&recipient_pubkey()),
},
InvoiceRequestTlvStreamRef {
chain: None,
amount: None,
features: None,
quantity: None,
payer_id: Some(&payer_pubkey()),
payer_note: None,
},
SignatureTlvStreamRef { signature: invoice_request.signature().as_ref() },
),
);

if let Err(e) = InvoiceRequest::try_from(buffer) {
panic!("error parsing invoice request: {:?}", e);
Expand Down
1 change: 1 addition & 0 deletions lightning/src/offers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ mod merkle;
pub mod offer;
pub mod parse;
mod payer;
pub mod refund;
58 changes: 33 additions & 25 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey: Some(signing_pubkey),
supported_quantity: Quantity::one(), signing_pubkey,
};
OfferBuilder { offer }
}
Expand Down Expand Up @@ -263,7 +263,7 @@ pub(super) struct OfferContents {
issuer: Option<String>,
paths: Option<Vec<BlindedPath>>,
supported_quantity: Quantity,
signing_pubkey: Option<PublicKey>,
signing_pubkey: PublicKey,
}

impl Offer {
Expand Down Expand Up @@ -359,7 +359,7 @@ impl Offer {

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

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

if node_id.is_none() {
return Err(SemanticError::MissingSigningPubkey);
}
let signing_pubkey = match node_id {
None => return Err(SemanticError::MissingSigningPubkey),
Some(node_id) => node_id,
};

Ok(OfferContents {
chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
supported_quantity, signing_pubkey: node_id,
supported_quantity, signing_pubkey,
})
}
}
Expand All @@ -653,7 +654,7 @@ impl core::fmt::Display for Offer {

#[cfg(test)]
mod tests {
use super::{Amount, Offer, OfferBuilder, Quantity};
use super::{Amount, Offer, OfferBuilder, OfferTlvStreamRef, Quantity};

use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
Expand All @@ -680,7 +681,7 @@ mod tests {
#[test]
fn builds_offer_with_defaults() {
let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
let tlv_stream = offer.as_tlv_stream();

let mut buffer = Vec::new();
offer.write(&mut buffer).unwrap();

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

assert_eq!(tlv_stream.chains, None);
assert_eq!(tlv_stream.metadata, None);
assert_eq!(tlv_stream.currency, None);
assert_eq!(tlv_stream.amount, None);
assert_eq!(tlv_stream.description, Some(&String::from("foo")));
assert_eq!(tlv_stream.features, None);
assert_eq!(tlv_stream.absolute_expiry, None);
assert_eq!(tlv_stream.paths, None);
assert_eq!(tlv_stream.issuer, None);
assert_eq!(tlv_stream.quantity_max, None);
assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
assert_eq!(
offer.as_tlv_stream(),
OfferTlvStreamRef {
chains: None,
metadata: None,
currency: None,
amount: None,
description: Some(&String::from("foo")),
features: None,
absolute_expiry: None,
paths: None,
issuer: None,
quantity_max: None,
node_id: Some(&pubkey(42)),
},
);

if let Err(e) = Offer::try_from(buffer) {
panic!("error parsing offer: {:?}", e);
Expand Down Expand Up @@ -1121,11 +1127,13 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut builder = OfferBuilder::new("foo".into(), pubkey(42));
builder.offer.signing_pubkey = None;
let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.node_id = None;

let offer = builder.build().unwrap();
match offer.to_string().parse::<Offer>() {
let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));
Expand Down
8 changes: 8 additions & 0 deletions lightning/src/offers/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,28 @@ pub enum SemanticError {
AlreadyExpired,
/// The provided chain hash does not correspond to a supported chain.
UnsupportedChain,
/// A chain was provided but was not expected.
UnexpectedChain,
/// An amount was expected but was missing.
MissingAmount,
/// The amount exceeded the total bitcoin supply.
InvalidAmount,
/// An amount was provided but was not sufficient in value.
InsufficientAmount,
/// An amount was provided but was not expected.
UnexpectedAmount,
/// A currency was provided that is not supported.
UnsupportedCurrency,
/// A feature was required but is unknown.
UnknownRequiredFeatures,
/// Features were provided but were not expected.
UnexpectedFeatures,
/// A required description was not provided.
MissingDescription,
/// A signing pubkey was not provided.
MissingSigningPubkey,
/// A signing pubkey was provided but was not expected.
UnexpectedSigningPubkey,
/// A quantity was expected but was missing.
MissingQuantity,
/// An unsupported quantity was provided.
Expand Down
Loading