--- /dev/null
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Data structures and encoding for static BOLT 12 invoices.
+
+use crate::blinded_path::BlindedPath;
+use crate::io;
+use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
+use crate::ln::msgs::DecodeError;
+use crate::offers::invoice::{
+ check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter,
+ BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef,
+};
+use crate::offers::invoice_macros::invoice_accessors_common;
+use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash};
+use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity};
+use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
+use crate::util::ser::{
+ HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
+};
+use crate::util::string::PrintableString;
+use bitcoin::address::Address;
+use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::secp256k1::PublicKey;
+use core::time::Duration;
+
+#[cfg(feature = "std")]
+use crate::offers::invoice::is_expired;
+
+#[allow(unused_imports)]
+use crate::prelude::*;
+
+/// Static invoices default to expiring after 2 weeks.
+const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14);
+
+/// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root.
+pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature");
+
+/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
+///
+/// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
+/// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
+/// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
+/// recipient is online to provide one.
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
+#[derive(Clone, Debug)]
+pub struct StaticInvoice {
+ bytes: Vec<u8>,
+ contents: InvoiceContents,
+ signature: Signature,
+}
+
+/// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
+///
+/// [`Offer`]: crate::offers::offer::Offer
+#[derive(Clone, Debug)]
+struct InvoiceContents {
+ offer: OfferContents,
+ payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
+ created_at: Duration,
+ relative_expiry: Option<Duration>,
+ fallbacks: Option<Vec<FallbackAddress>>,
+ features: Bolt12InvoiceFeatures,
+ signing_pubkey: PublicKey,
+ message_paths: Vec<BlindedPath>,
+}
+
+macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
+ /// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
+ /// created from offers that support a single chain.
+ pub fn chain(&$self) -> ChainHash {
+ $contents.chain()
+ }
+
+ /// Opaque bytes set by the originating [`Offer::metadata`].
+ ///
+ /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
+ pub fn metadata(&$self) -> Option<&Vec<u8>> {
+ $contents.metadata()
+ }
+
+ /// The minimum amount required for a successful payment of a single item.
+ ///
+ /// From [`Offer::amount`].
+ ///
+ /// [`Offer::amount`]: crate::offers::offer::Offer::amount
+ pub fn amount(&$self) -> Option<Amount> {
+ $contents.amount()
+ }
+
+ /// Features pertaining to the originating [`Offer`], from [`Offer::offer_features`].
+ ///
+ /// [`Offer`]: crate::offers::offer::Offer
+ /// [`Offer::offer_features`]: crate::offers::offer::Offer::offer_features
+ pub fn offer_features(&$self) -> &OfferFeatures {
+ $contents.offer_features()
+ }
+
+ /// A complete description of the purpose of the originating offer, from [`Offer::description`].
+ ///
+ /// [`Offer::description`]: crate::offers::offer::Offer::description
+ pub fn description(&$self) -> Option<PrintableString> {
+ $contents.description()
+ }
+
+ /// Duration since the Unix epoch when an invoice should no longer be requested, from
+ /// [`Offer::absolute_expiry`].
+ ///
+ /// [`Offer::absolute_expiry`]: crate::offers::offer::Offer::absolute_expiry
+ pub fn absolute_expiry(&$self) -> Option<Duration> {
+ $contents.absolute_expiry()
+ }
+
+ /// The issuer of the offer, from [`Offer::issuer`].
+ ///
+ /// [`Offer::issuer`]: crate::offers::offer::Offer::issuer
+ pub fn issuer(&$self) -> Option<PrintableString> {
+ $contents.issuer()
+ }
+
+ /// Paths to the node that may supply the invoice on the recipient's behalf, originating from
+ /// publicly reachable nodes. Taken from [`Offer::paths`].
+ ///
+ /// [`Offer::paths`]: crate::offers::offer::Offer::paths
+ pub fn offer_message_paths(&$self) -> &[BlindedPath] {
+ $contents.offer_message_paths()
+ }
+
+ /// Paths to the recipient for indicating that a held HTLC is available to claim when they next
+ /// come online.
+ pub fn message_paths(&$self) -> &[BlindedPath] {
+ $contents.message_paths()
+ }
+
+ /// The quantity of items supported, from [`Offer::supported_quantity`].
+ ///
+ /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity
+ pub fn supported_quantity(&$self) -> Quantity {
+ $contents.supported_quantity()
+ }
+} }
+
+impl StaticInvoice {
+ invoice_accessors_common!(self, self.contents, StaticInvoice);
+ invoice_accessors!(self, self.contents);
+
+ /// Signature of the invoice verified using [`StaticInvoice::signing_pubkey`].
+ pub fn signature(&self) -> Signature {
+ self.signature
+ }
+}
+
+impl InvoiceContents {
+ fn chain(&self) -> ChainHash {
+ debug_assert_eq!(self.offer.chains().len(), 1);
+ self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain())
+ }
+
+ fn metadata(&self) -> Option<&Vec<u8>> {
+ self.offer.metadata()
+ }
+
+ fn amount(&self) -> Option<Amount> {
+ self.offer.amount()
+ }
+
+ fn offer_features(&self) -> &OfferFeatures {
+ self.offer.features()
+ }
+
+ fn description(&self) -> Option<PrintableString> {
+ self.offer.description()
+ }
+
+ fn absolute_expiry(&self) -> Option<Duration> {
+ self.offer.absolute_expiry()
+ }
+
+ fn issuer(&self) -> Option<PrintableString> {
+ self.offer.issuer()
+ }
+
+ fn offer_message_paths(&self) -> &[BlindedPath] {
+ self.offer.paths()
+ }
+
+ fn message_paths(&self) -> &[BlindedPath] {
+ &self.message_paths[..]
+ }
+
+ fn supported_quantity(&self) -> Quantity {
+ self.offer.supported_quantity()
+ }
+
+ fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] {
+ &self.payment_paths[..]
+ }
+
+ fn created_at(&self) -> Duration {
+ self.created_at
+ }
+
+ fn relative_expiry(&self) -> Duration {
+ self.relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
+ }
+
+ #[cfg(feature = "std")]
+ fn is_expired(&self) -> bool {
+ is_expired(self.created_at(), self.relative_expiry())
+ }
+
+ fn fallbacks(&self) -> Vec<Address> {
+ let chain = self.chain();
+ self.fallbacks
+ .as_ref()
+ .map(|fallbacks| filter_fallbacks(chain, fallbacks))
+ .unwrap_or_else(Vec::new)
+ }
+
+ fn features(&self) -> &Bolt12InvoiceFeatures {
+ &self.features
+ }
+
+ fn signing_pubkey(&self) -> PublicKey {
+ self.signing_pubkey
+ }
+}
+
+impl Writeable for StaticInvoice {
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ WithoutLength(&self.bytes).write(writer)
+ }
+}
+
+impl TryFrom<Vec<u8>> for StaticInvoice {
+ type Error = Bolt12ParseError;
+
+ fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+ let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
+ StaticInvoice::try_from(parsed_invoice)
+ }
+}
+
+type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream);
+
+impl SeekReadable for FullInvoiceTlvStream {
+ fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+ let offer = SeekReadable::read(r)?;
+ let invoice = SeekReadable::read(r)?;
+ let signature = SeekReadable::read(r)?;
+
+ Ok((offer, invoice, signature))
+ }
+}
+
+type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream);
+
+impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
+ type Error = Bolt12ParseError;
+
+ fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
+ let ParsedMessage { bytes, tlv_stream } = invoice;
+ let (offer_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }) = tlv_stream;
+ let contents = InvoiceContents::try_from((offer_tlv_stream, invoice_tlv_stream))?;
+
+ let signature = match signature {
+ None => {
+ return Err(Bolt12ParseError::InvalidSemantics(
+ Bolt12SemanticError::MissingSignature,
+ ))
+ },
+ Some(signature) => signature,
+ };
+ let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
+ let pubkey = contents.signing_pubkey;
+ merkle::verify_signature(&signature, &tagged_hash, pubkey)?;
+
+ Ok(StaticInvoice { bytes, contents, signature })
+ }
+}
+
+impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
+ type Error = Bolt12SemanticError;
+
+ fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
+ let (
+ offer_tlv_stream,
+ InvoiceTlvStream {
+ paths,
+ blindedpay,
+ created_at,
+ relative_expiry,
+ fallbacks,
+ features,
+ node_id,
+ message_paths,
+ payment_hash,
+ amount,
+ },
+ ) = tlv_stream;
+
+ if payment_hash.is_some() {
+ return Err(Bolt12SemanticError::UnexpectedPaymentHash);
+ }
+ if amount.is_some() {
+ return Err(Bolt12SemanticError::UnexpectedAmount);
+ }
+
+ let payment_paths = construct_payment_paths(blindedpay, paths)?;
+ let message_paths = message_paths.ok_or(Bolt12SemanticError::MissingPaths)?;
+
+ let created_at = match created_at {
+ None => return Err(Bolt12SemanticError::MissingCreationTime),
+ Some(timestamp) => Duration::from_secs(timestamp),
+ };
+
+ let relative_expiry = relative_expiry.map(Into::<u64>::into).map(Duration::from_secs);
+
+ let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
+
+ let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
+ check_invoice_signing_pubkey(&signing_pubkey, &offer_tlv_stream)?;
+
+ if offer_tlv_stream.paths.is_none() {
+ return Err(Bolt12SemanticError::MissingPaths);
+ }
+ if offer_tlv_stream.chains.as_ref().map_or(0, |chains| chains.len()) > 1 {
+ return Err(Bolt12SemanticError::UnexpectedChain);
+ }
+
+ Ok(InvoiceContents {
+ offer: OfferContents::try_from(offer_tlv_stream)?,
+ payment_paths,
+ message_paths,
+ created_at,
+ relative_expiry,
+ fallbacks,
+ features,
+ signing_pubkey,
+ })
+ }
+}