diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b018c6c74cd..6b1d4a1fd5f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -85,9 +85,10 @@ use crate::ln::outbound_payment::{ SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; -use crate::offers::flow::OffersMessageFlow; +use crate::offers::flow::{FlowConfigs, OffersMessageFlow}; use crate::offers::invoice::{ - Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, + Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, InvoiceBuilderVariant, + UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY, }; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::InvoiceRequest; @@ -155,7 +156,6 @@ use { }; #[cfg(not(c_bindings))] use { - crate::offers::offer::{DerivedMetadata, OfferBuilder}, crate::offers::refund::RefundBuilder, crate::onion_message::messenger::DefaultMessageRouter, crate::routing::gossip::NetworkGraph, @@ -164,6 +164,9 @@ use { crate::sign::KeysManager, }; +#[cfg(any(not(c_bindings), async_payments))] +use crate::offers::offer::{DerivedMetadata, OfferBuilder}; + use lightning_invoice::{ Bolt11Invoice, Bolt11InvoiceDescription, CreationError, Currency, Description, InvoiceBuilder as Bolt11InvoiceBuilder, SignOrCreationError, DEFAULT_EXPIRY_TIME, @@ -3702,9 +3705,51 @@ where let flow = OffersMessageFlow::new( ChainHash::using_genesis_block(params.network), params.best_block, our_network_pubkey, current_timestamp, expanded_inbound_key, - secp_ctx.clone(), message_router + secp_ctx.clone(), message_router, FlowConfigs::new() ); + Self::new_inner( + secp_ctx, fee_est, chain_monitor, tx_broadcaster, router, flow, + logger, entropy_source, node_signer, signer_provider, config, params, + current_timestamp + ) + + } + + /// Similar to [`ChannelManager::new`], but allows providing a custom [`OffersMessageFlow`] implementation. + /// + /// This is useful if you want more control over how BOLT12 offer-related messages are handled, + /// including support for custom [`FlowConfigs`] to conditionally trigger [`OfferEvents`] that you + /// can handle asynchronously via your own logic. + /// + /// Use this method when: + /// - You want to initialize [`ChannelManager`] with a non-default [`OffersMessageFlow`] implementation. + /// - You need fine-grained control over BOLT12 event generation or message flow behavior. + /// + /// [`FlowConfigs`]: crate::offers::flow::FlowConfigs + /// [`OfferEvents`]: crate::offers::flow::OfferEvents + #[rustfmt::skip] + pub fn new_with_flow( + fee_est: F, chain_monitor: M, tx_broadcaster: T, router: R, flow: OffersMessageFlow, + logger: L, entropy_source: ES, node_signer: NS, signer_provider: SP, config: UserConfig, + params: ChainParameters, current_timestamp: u32, + ) -> Self { + let mut secp_ctx = Secp256k1::new(); + secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); + + Self::new_inner( + secp_ctx, fee_est, chain_monitor, tx_broadcaster, router, flow, + logger, entropy_source, node_signer, signer_provider, config, params, + current_timestamp + ) + } + + #[rustfmt::skip] + fn new_inner( + secp_ctx: Secp256k1, fee_est: F, chain_monitor: M, tx_broadcaster: T, router: R, + flow: OffersMessageFlow, logger: L, entropy_source: ES, node_signer: NS, + signer_provider: SP, config: UserConfig, params: ChainParameters, current_timestamp: u32 + ) -> Self { ChannelManager { default_configuration: config.clone(), chain_hash: ChainHash::using_genesis_block(params.network), @@ -3724,10 +3769,10 @@ where pending_intercepted_htlcs: Mutex::new(new_hash_map()), short_to_chan_info: FairRwLock::new(new_hash_map()), - our_network_pubkey, + our_network_pubkey: node_signer.get_node_id(Recipient::Node).unwrap(), secp_ctx, - inbound_payment_key: expanded_inbound_key, + inbound_payment_key: node_signer.get_inbound_payment_key(), fake_scid_rand_bytes: entropy_source.get_secure_random_bytes(), probing_cookie_secret: entropy_source.get_secure_random_bytes(), @@ -12680,8 +12725,17 @@ where Err(_) => return None, }; + let invoice_request = match self.flow.determine_invoice_request_handling(invoice_request) { + Ok(Some(ir)) => ir, + Ok(None) => return None, + Err(_) => { + log_trace!(self.logger, "Failed to handle invoice request"); + return None; + } + }; + let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner + &invoice_request.inner, None ) { Ok(amount_msats) => amount_msats, Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), @@ -12699,15 +12753,55 @@ where }; let entropy = &*self.entropy_source; - let (response, context) = self.flow.create_response_for_invoice_request( - &self.node_signer, &self.router, entropy, invoice_request, amount_msats, - payment_hash, payment_secret, self.list_usable_channels() - ); - match context { - Some(context) => Some((response, responder.respond_with_reply_path(context))), - None => Some((response, responder.respond())) - } + let (builder_var, context) = match self.flow.create_invoice_builder_from_invoice_request( + &self.router, + entropy, + &invoice_request, + amount_msats, + payment_hash, + payment_secret, + self.list_usable_channels(), + ) { + Ok(result) => result, + Err(error) => { + return Some(( + OffersMessage::InvoiceError(InvoiceError::from(error)), + responder.respond(), + )); + } + }; + + let result = match builder_var { + InvoiceBuilderVariant::Derived(builder) => { + builder + .build_and_sign(&self.secp_ctx) + .map_err(InvoiceError::from) + }, + InvoiceBuilderVariant::Explicit(builder) => { + builder + .build() + .map_err(InvoiceError::from) + .and_then(|invoice| { + #[cfg(c_bindings)] + let mut invoice = invoice; + invoice + .sign(|invoice: &UnsignedBolt12Invoice| self.node_signer.sign_bolt12_invoice(invoice)) + .map_err(InvoiceError::from) + }) + } + }; + + Some(match result { + Ok(invoice) => ( + OffersMessage::Invoice(invoice), + responder.respond_with_reply_path(context), + ), + Err(error) => ( + OffersMessage::InvoiceError(error), + responder.respond(), + ), + }) }, OffersMessage::Invoice(invoice) => { let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) { @@ -12719,6 +12813,15 @@ where &self.logger, None, None, Some(invoice.payment_hash()), ); + let invoice = match self.flow.determine_invoice_handling(invoice, payment_id) { + Ok(Some(invoice)) => invoice, + Ok(None) => return None, + Err(_) => { + log_trace!(logger, "Failed to handle invoice"); + return None + }, + }; + if self.default_configuration.manually_handle_bolt12_invoices { // Update the corresponding entry in `PendingOutboundPayment` for this invoice. // This ensures that event generation remains idempotent in case we receive @@ -14924,7 +15027,7 @@ where let flow = OffersMessageFlow::new( chain_hash, best_block, our_network_pubkey, highest_seen_timestamp, expanded_inbound_key, - secp_ctx.clone(), args.message_router + secp_ctx.clone(), args.message_router, FlowConfigs::new(), ); let channel_manager = ChannelManager { diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index ad0a8eea2aa..649055eaf43 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2231,7 +2231,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let created_at = alice.node.duration_since_epoch(); let invoice = invoice_request .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() - .respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap() + .respond_using_derived_keys_no_std(None, payment_paths, payment_hash, created_at).unwrap() .features_unchecked(Bolt12InvoiceFeatures::unknown()) .build_and_sign(&secp_ctx).unwrap(); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 1beeb3574db..de7362b082b 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1114,7 +1114,7 @@ impl OutboundPayments { return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired)) } - let amount_msat = match InvoiceBuilder::::amount_msats(invreq) { + let amount_msat = match InvoiceBuilder::::amount_msats(invreq, None) { Ok(amt) => amt, Err(_) => { // We check this during invoice request parsing, when constructing the invreq's @@ -2975,7 +2975,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap() + .respond_with_no_std(None, payment_paths(), payment_hash(), created_at).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3021,7 +3021,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(None, payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3083,7 +3083,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(None, payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 38f674141b1..f932572b046 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -37,15 +37,14 @@ use crate::ln::channelmanager::{ }; use crate::ln::inbound_payment; use crate::offers::invoice::{ - Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, - UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY, + Bolt12Invoice, Bolt12InvoiceAmountSource, DerivedSigningPubkey, InvoiceBuilder, + InvoiceBuilderVariant, DEFAULT_RELATIVE_EXPIRY, }; -use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{ - InvoiceRequest, InvoiceRequestBuilder, VerifiedInvoiceRequest, + InvoiceRequest, InvoiceRequestAmountSource, InvoiceRequestBuilder, VerifiedInvoiceRequest, }; use crate::offers::nonce::Nonce; -use crate::offers::offer::{DerivedMetadata, Offer, OfferBuilder}; +use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; use crate::onion_message::async_payments::AsyncPaymentsMessage; @@ -53,7 +52,7 @@ use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendIns use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::OnionMessageContents; use crate::routing::router::Router; -use crate::sign::{EntropySource, NodeSigner}; +use crate::sign::EntropySource; use crate::sync::{Mutex, RwLock}; use crate::types::payment::{PaymentHash, PaymentSecret}; @@ -61,7 +60,6 @@ use crate::types::payment::{PaymentHash, PaymentSecret}; use { crate::blinded_path::message::AsyncPaymentsContext, crate::blinded_path::payment::AsyncBolt12OfferContext, - crate::offers::offer::Amount, crate::offers::signer, crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, crate::onion_message::async_payments::HeldHtlcAvailable, @@ -73,6 +71,230 @@ use { crate::onion_message::dns_resolution::{DNSResolverMessage, DNSSECQuery, OMNameResolver}, }; +/// Contains OfferEvents that can optionally triggered for manual +/// handling by user based on appropriate user_config. +pub enum OfferEvents { + /// Notifies that a verified [`InvoiceRequest`] has been received. + /// + /// This event is triggered when a BOLT12 [`InvoiceRequest`] is received. + /// The `amount_source` field describes how the amount is specified — in the offer, + /// the invoice request, or both. + /// + /// To respond, use [`OffersMessageFlow::create_invoice_builder_from_invoice_request`], providing an + /// amount based on the structure described in [`InvoiceRequestAmountSource`]. + /// + /// See the [BOLT12 spec](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md#requirements-1) + /// for protocol-level details. + InvoiceRequestReceived { + /// The verified [`InvoiceRequest`] that was received. + invoice_request: VerifiedInvoiceRequest, + + /// Indicates how the amount is specified across the [`InvoiceRequest`] and the associated [`Offer`] (if any). + /// + /// Use this to determine what amount to pass when calling [`OffersMessageFlow::create_invoice_builder_from_invoice_request`]. + /// + /// ### [`InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount`] + /// Both the [`InvoiceRequest`] and the [`Offer`] specify amounts. + /// + /// - If the offer uses [`Amount::Currency`], ensure that the request amount reasonably compensates + /// based on conversion rates. + /// + /// - If the offer uses [`Amount::Bitcoin`], this implementation ensures the request amount is + /// **greater than or equal to** the offer amount before constructing this variant. + /// + /// In either case, use the exact request amount when calling [`OffersMessageFlow::create_invoice_builder_from_invoice_request`]. + /// + /// ### [`InvoiceRequestAmountSource::InvoiceRequestOnly`] + /// The [`InvoiceRequest`] specifies an amount, and the associated [`Offer`] does not. + /// + /// - You must pass the exact request amount to [`OffersMessageFlow::create_invoice_builder_from_invoice_request`] when responding. + /// + /// ### [`InvoiceRequestAmountSource::OfferOnly`] + /// The [`InvoiceRequest`] does not specify an amount. The amount is taken solely from the associated [`Offer`]. + /// + /// - If the amount is in [`Amount::Currency`], user must convert it manually into a Bitcoin-denominated amount + /// and then pass it to [`OffersMessageFlow::create_invoice_builder_from_invoice_request`]. + /// + /// - If the amount is in [`Amount::Bitcoin`], user must provide an amount that is **greater than or equal to** + /// the offer amount when calling the builder function. + /// + /// [`Amount::Currency`]: crate::offers::offer::Amount::Currency + /// [`Amount::Bitcoin`]: crate::offers::offer::Amount::Bitcoin + amount_source: InvoiceRequestAmountSource, + }, + + /// Notified that an [`Bolt12Invoice`] was received + /// This event is triggered when a BOLT12 [`Bolt12Invoice`] is received. + /// + /// User must use their custom logic to pay the invoice, using the exact amount specified in the invoice. + Bolt12InvoiceReceived { + /// The verified [`Bolt12Invoice`] that was received. + invoice: Bolt12Invoice, + + /// The [`PaymentId`] associated with the invoice. + payment_id: PaymentId, + + /// Indicates how the amount is specified in the invoice. + invoice_amount: u64, + + /// Indicates the source of the amount: whether from Offer, Refund, or InvoiceRequest. + /// Useful to examine the invoice's amount before deciding to pay it. + /// + /// ## [`Bolt12InvoiceAmountSource::Offer`] + /// + /// If the invoice correponds to an Offer flow, it could be based on following cases: + /// + /// ### [`InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount`]: + /// + /// - If the offer uses [`Amount::Currency`], ensure that the request amount reasonably compensates + /// based on conversion rates. + /// + /// - If the offer uses [`Amount::Bitcoin`], this implementation ensures the request amount is + /// **greater than or equal to** the offer amount before constructing this variant. + /// + /// In both cases, the implementation ensures that the invoice amount is exactly equal to the request amount. + /// + /// ### [`InvoiceRequestAmountSource::InvoiceRequestOnly`]: + /// + /// If only the [`InvoiceRequest`] amount is specified, implementation ensures that the invoice amount is exactly + /// equal to the request amount abiding by the spec. + /// + /// ### [`InvoiceRequestAmountSource::OfferOnly`]: + /// + /// - If the offer amount is in [`Amount::Currency`], the user must ensures that the invoice amount sufficiently + /// compensates for the offer amount post conversion. + /// - If the offer amount is in [`Amount::Bitcoin`], the implementation ensures that the invoice amount is exactly + /// equal to the offer amount. + /// + /// For more details, see the [BOLT12 spec](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md#requirements-1) + /// + /// ## [`Bolt12InvoiceAmountSource::Refund`] + /// + /// The invoice corresponds to a [`Refund`] flow, with the amount specified in the [`Refund`]. + /// + /// The amount is specified in the [`Refund`] and must be equal to the invoice amount, which the implementation ensures. + /// + /// [`Amount::Currency`]: crate::offers::offer::Amount::Currency + /// [`Amount::Bitcoin`]: crate::offers::offer::Amount::Bitcoin + amount_source: Bolt12InvoiceAmountSource, + }, +} + +/// Configuration options for determining which [`OfferEvents`] should be generated during BOLT12 offer handling. +/// +/// Use this to control whether events such as [`OfferEvents::InvoiceRequestReceived`] and +/// [`OfferEvents::Bolt12InvoiceReceived`] are triggered automatically or suppressed, depending on your use case. +/// +/// The default behavior disables all events (`NeverTrigger`) for both cases. +pub struct FlowConfigs { + /// Controls whether [`OfferEvents::InvoiceRequestReceived`] is generated upon receiving an [`InvoiceRequest`]. + invoice_request_configs: InvoiceRequestConfigs, + + /// Controls whether [`OfferEvents::Bolt12InvoiceReceived`] is generated upon receiving a [`Bolt12Invoice`]. + invoice_configs: Bolt12InvoiceConfigs, +} + +impl FlowConfigs { + /// Creates a new [`FlowConfigs`] instance with default settings. + /// + /// By default, all events are set to `NeverTrigger`, meaning no events will be generated. + pub fn new() -> Self { + Self { + invoice_request_configs: InvoiceRequestConfigs::NeverTrigger, + invoice_configs: Bolt12InvoiceConfigs::NeverTrigger, + } + } + + /// Sets the configuration for invoice request events. + pub fn set_invoice_request_configs(self, configs: InvoiceRequestConfigs) -> Self { + Self { invoice_request_configs: configs, ..self } + } + + /// Sets the configuration for invoice events. + pub fn set_invoice_configs(self, configs: Bolt12InvoiceConfigs) -> Self { + Self { invoice_configs: configs, ..self } + } + + /// Determines whether an [`InvoiceRequest`] should be handled synchronously or dispatched as an event. + pub fn handle_invoice_request_asyncly( + &self, invoice_request: &InvoiceRequest, + ) -> Result { + let amount_source = invoice_request.contents.amount_source().map_err(|_| ())?; + + let config = &self.invoice_request_configs; + Ok(match config { + InvoiceRequestConfigs::AlwaysTrigger => true, + InvoiceRequestConfigs::NeverTrigger => false, + InvoiceRequestConfigs::TriggerIfOfferInCurrency => match amount_source { + InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount { + offer_amount, .. + } + | InvoiceRequestAmountSource::OfferOnly { amount: offer_amount } => { + matches!(offer_amount, Amount::Currency { .. }) + }, + InvoiceRequestAmountSource::InvoiceRequestOnly { .. } => false, + }, + }) + } + + /// Determines whether an [`Bolt12Invoice`] should be handled synchronously or dispatched as an event. + pub fn handle_invoice_asyncly(&self, invoice: &Bolt12Invoice) -> Result { + let amount_source = invoice.amount_source().map_err(|_| ())?; + let (ir_amount, offer_amount) = match amount_source { + Bolt12InvoiceAmountSource::Offer(amount) => match amount { + InvoiceRequestAmountSource::OfferOnly { amount } => (None, Some(amount)), + InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount { + invoice_request_amount_msats, + offer_amount, + } => (Some(invoice_request_amount_msats), Some(offer_amount)), + InvoiceRequestAmountSource::InvoiceRequestOnly { amount_msats } => { + (Some(amount_msats), None) + }, + }, + Bolt12InvoiceAmountSource::Refund(_) => (None, None), + }; + Ok(match self.invoice_configs { + Bolt12InvoiceConfigs::AlwaysTrigger => true, + Bolt12InvoiceConfigs::TriggerIfOfferInCurrency => { + matches!(offer_amount, Some(Amount::Currency { .. })) + }, + Bolt12InvoiceConfigs::TriggerIfOfferInCurrencyAndNoIRAmount => { + matches!(offer_amount, Some(Amount::Currency { .. })) && ir_amount.is_none() + }, + Bolt12InvoiceConfigs::NeverTrigger => false, + }) + } +} + +/// Specifies under what conditions an [`InvoiceRequest`] will generate an [`OfferEvents::InvoiceRequestReceived`] event. +pub enum InvoiceRequestConfigs { + /// Always trigger the event when an [`InvoiceRequest`] is received. + AlwaysTrigger, + + /// Trigger the event only if the corresponding [`Offer`] specifies an [`Amount::Currency`] amount. + TriggerIfOfferInCurrency, + + /// Never trigger the event, regardless of the incoming [`InvoiceRequest`]. + NeverTrigger, +} + +/// Specifies under what conditions a [`Bolt12Invoice`] will generate an [`OfferEvents::Bolt12InvoiceReceived`] event. +pub enum Bolt12InvoiceConfigs { + /// Always trigger the event when a [`Bolt12Invoice`] is received. + AlwaysTrigger, + + /// Trigger the event only if the invoice corresponds to an [`Offer`] flow with an [`Amount::Currency`] offer. + TriggerIfOfferInCurrency, + + /// Trigger the event only if the invoice corresponds to an [`Offer`] flow where: + /// - the underlying [`Offer`] amount is in [`Amount::Currency`], and + /// - the corresponding [`InvoiceRequest`] did **not** specify an amount. + TriggerIfOfferInCurrencyAndNoIRAmount, + + /// Never trigger the event, regardless of the incoming [`Bolt12Invoice`]. + NeverTrigger, +} + /// A BOLT12 offers code and flow utility provider, which facilitates /// BOLT12 builder generation and onion message handling. /// @@ -97,12 +319,16 @@ where #[cfg(any(test, feature = "_test_utils"))] pub(crate) pending_offers_messages: Mutex>, + pending_offers_events: Mutex>, + pending_async_payments_messages: Mutex>, #[cfg(feature = "dnssec")] pub(crate) hrn_resolver: OMNameResolver, #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex>, + + user_configs: FlowConfigs, } impl OffersMessageFlow @@ -113,7 +339,7 @@ where pub fn new( chain_hash: ChainHash, best_block: BestBlock, our_network_pubkey: PublicKey, current_timestamp: u32, inbound_payment_key: inbound_payment::ExpandedKey, - secp_ctx: Secp256k1, message_router: MR, + secp_ctx: Secp256k1, message_router: MR, configs: FlowConfigs, ) -> Self { Self { chain_hash, @@ -127,12 +353,15 @@ where message_router, pending_offers_messages: Mutex::new(Vec::new()), + pending_offers_events: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), #[cfg(feature = "dnssec")] hrn_resolver: OMNameResolver::new(current_timestamp, best_block.height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + user_configs: configs, } } @@ -341,6 +570,62 @@ impl OffersMessageFlow where MR::Target: MessageRouter, { + /// Determines whether the given [`VerifiedInvoiceRequest`] should be + /// handled synchronously or dispatched as an event, based on [`FlowConfigs`]. + /// + /// Returns: + /// - `Ok(Some(request))` if the caller should handle it now. + /// - `Ok(None)` if it was dispatched for async processing. + /// - `Err(())` in case of validation or enqueue failure. + pub fn determine_invoice_request_handling( + &self, invoice_request: VerifiedInvoiceRequest, + ) -> Result, ()> { + if !self.user_configs.handle_invoice_request_asyncly(&invoice_request.inner)? { + // Synchronous path: return the request for user handling. + return Ok(Some(invoice_request)); + } + + let amount_source = invoice_request.inner.contents.amount_source().map_err(|_| ())?; + + // Dispatch event for async handling. + self.enqueue_offers_event(OfferEvents::InvoiceRequestReceived { + invoice_request, + amount_source, + })?; + + Ok(None) + } + + /// Determines whether the given [`Bolt12Invoice`] should be handled + /// synchronously or dispatched as an event, based on [`FlowConfigs`]. + /// + /// Returns: + /// - `Ok(Some(request))` if the caller should handle it now. + /// - `Ok(None)` if it was dispatched for async processing. + /// - `Err(())` in case of validation or enqueue failure. + pub fn determine_invoice_handling( + &self, invoice: Bolt12Invoice, payment_id: PaymentId, + ) -> Result, ()> { + if !self.user_configs.handle_invoice_asyncly(&invoice)? { + // Synchronous path: return the invoice for user handling. + return Ok(Some(invoice)); + } + + let invoice_amount = invoice.amount_msats(); + let amount_source = invoice.amount_source().map_err(|_| ())?; + + let event = OfferEvents::Bolt12InvoiceReceived { + invoice, + payment_id, + invoice_amount, + amount_source, + }; + + self.enqueue_offers_event(event)?; + + Ok(None) + } + /// Verifies an [`InvoiceRequest`] using the provided [`OffersContext`] or the [`InvoiceRequest::metadata`]. /// /// - If an [`OffersContext::InvoiceRequest`] with a `nonce` is provided, verification is performed using recipient context data. @@ -763,27 +1048,30 @@ where Ok(builder.into()) } - /// Creates a response for the provided [`VerifiedInvoiceRequest`]. + /// Creates an [`InvoiceBuilderVariant`] for the provided [`VerifiedInvoiceRequest`]. /// - /// A response can be either an [`OffersMessage::Invoice`] with additional [`MessageContext`], - /// or an [`OffersMessage::InvoiceError`], depending on the [`InvoiceRequest`]. + /// Returns the appropriate invoice builder variant (`Derived` or `Explicit`) along with a + /// [`MessageContext`] that can later be used to respond to the counterparty. /// - /// An [`OffersMessage::InvoiceError`] will be generated if: - /// - We fail to generate valid payment paths to include in the [`Bolt12Invoice`]. - /// - We fail to generate a valid signed [`Bolt12Invoice`] for the [`InvoiceRequest`]. - pub fn create_response_for_invoice_request( - &self, signer: &NS, router: &R, entropy_source: ES, - invoice_request: VerifiedInvoiceRequest, amount_msats: u64, payment_hash: PaymentHash, - payment_secret: PaymentSecret, usable_channels: Vec, - ) -> (OffersMessage, Option) + /// Use this method when you want to inspect or modify the [`InvoiceBuilder`] before signing and + /// generating the final [`Bolt12Invoice`]. + /// + /// # Errors + /// + /// Returns a [`Bolt12SemanticError`] if: + /// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`]. + /// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`]. + pub fn create_invoice_builder_from_invoice_request<'a, ES: Deref, R: Deref>( + &'a self, router: &R, entropy_source: ES, invoice_request: &'a VerifiedInvoiceRequest, + amount_msats: u64, payment_hash: PaymentHash, payment_secret: PaymentSecret, + usable_channels: Vec, + ) -> Result<(InvoiceBuilderVariant<'a>, MessageContext), Bolt12SemanticError> where ES::Target: EntropySource, - NS::Target: NodeSigner, R::Target: Router, { let entropy = &*entropy_source; let expanded_key = &self.inbound_payment_key; - let secp_ctx = &self.secp_ctx; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; @@ -792,70 +1080,51 @@ where invoice_request: invoice_request.fields(), }); - let payment_paths = match self.create_blinded_payment_paths( - router, - entropy, - usable_channels, - Some(amount_msats), - payment_secret, - context, - relative_expiry, - ) { - Ok(paths) => paths, - Err(_) => { - let error = InvoiceError::from(Bolt12SemanticError::MissingPaths); - return (OffersMessage::InvoiceError(error.into()), None); - }, - }; + let payment_paths = self + .create_blinded_payment_paths( + router, + entropy, + usable_channels, + Some(amount_msats), + payment_secret, + context, + relative_expiry, + ) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; #[cfg(not(feature = "std"))] let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); - let response = if invoice_request.keys.is_some() { + let builder = if invoice_request.keys.is_some() { #[cfg(feature = "std")] - let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash); + let builder = invoice_request.respond_using_derived_keys( + Some(amount_msats), + payment_paths, + payment_hash, + ); #[cfg(not(feature = "std"))] let builder = invoice_request.respond_using_derived_keys_no_std( + Some(amount_msats), payment_paths, payment_hash, created_at, ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) - .map_err(InvoiceError::from) + + builder.map(|b| InvoiceBuilderVariant::Derived(InvoiceBuilder::from(b).allow_mpp()))? } else { #[cfg(feature = "std")] let builder = invoice_request.respond_with(payment_paths, payment_hash); #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_with_no_std(payment_paths, payment_hash, created_at); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build()) - .map_err(InvoiceError::from) - .and_then(|invoice| { - #[cfg(c_bindings)] - let mut invoice = invoice; - invoice - .sign(|invoice: &UnsignedBolt12Invoice| signer.sign_bolt12_invoice(invoice)) - .map_err(InvoiceError::from) - }) + let builder = invoice_request.respond_with_no_std(None, payment_paths, payment_hash, created_at); + builder.map(|b| InvoiceBuilderVariant::Explicit(InvoiceBuilder::from(b).allow_mpp()))? }; - match response { - Ok(invoice) => { - let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { - payment_hash, - nonce, - hmac, - }); - - (OffersMessage::Invoice(invoice), Some(context)) - }, - Err(error) => (OffersMessage::InvoiceError(error.into()), None), - } + let nonce = Nonce::from_entropy_source(entropy); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let context = + MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); + + Ok((builder, context)) } /// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty. @@ -883,6 +1152,7 @@ where /// or [`InvoiceError`]. /// /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError pub fn enqueue_invoice_request( &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, peers: Vec, @@ -935,6 +1205,7 @@ where /// to create blinded reply paths /// /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError pub fn enqueue_invoice( &self, entropy_source: ES, invoice: Bolt12Invoice, refund: &Refund, peers: Vec, @@ -1063,6 +1334,13 @@ where Ok(()) } + /// Enqueues an [`OfferEvents`] event to be processed manually by the user. + pub fn enqueue_offers_event(&self, event: OfferEvents) -> Result<(), ()> { + let mut pending_offers_events = self.pending_offers_events.lock().unwrap(); + pending_offers_events.push(event); + Ok(()) + } + /// Gets the enqueued [`OffersMessage`] with their corresponding [`MessageSendInstructions`]. pub fn release_pending_offers_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) @@ -1082,4 +1360,9 @@ where ) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) } + + /// Gets the enqueued [`OfferEvents`] events. + pub fn get_and_clear_pending_offers_events(&self) -> Vec { + core::mem::take(&mut self.pending_offers_events.lock().unwrap()) + } } diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 3615850a22e..ec94ea8b066 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -56,7 +56,7 @@ #![cfg_attr( not(feature = "std"), doc = " - .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? + .respond_with_no_std(None, payment_paths, payment_hash, core::time::Duration::from_secs(0))? " )] //! # ) @@ -95,7 +95,7 @@ #![cfg_attr( not(feature = "std"), doc = " - .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? + .respond_with_no_std(None, payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? " )] //! # ) @@ -126,9 +126,9 @@ use crate::offers::invoice_macros::invoice_builder_methods_test_common; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::invoice_request::{ ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequest, - InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, - EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, - IV_BYTES as INVOICE_REQUEST_IV_BYTES, + InvoiceRequestAmountSource, InvoiceRequestContents, InvoiceRequestTlvStream, + InvoiceRequestTlvStreamRef, EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, + INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, }; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, @@ -220,6 +220,16 @@ pub struct InvoiceWithDerivedSigningPubkeyBuilder<'a> { signing_pubkey_strategy: DerivedSigningPubkey, } +/// A variant of [`InvoiceBuilder`] that indicates how the signing public key was set. +/// +/// This is not exported to bindings users as builder patterns don't map outside of move semantics. +pub enum InvoiceBuilderVariant<'a> { + /// An [`InvoiceBuilder`] that uses a derived signing public key. + Derived(InvoiceBuilder<'a, DerivedSigningPubkey>), + /// An [`InvoiceBuilder`] that uses an explicitly set signing public key. + Explicit(InvoiceBuilder<'a, ExplicitSigningPubkey>), +} + /// Indicates how [`Bolt12Invoice::signing_pubkey`] was set. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. @@ -242,10 +252,11 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_offer( - invoice_request: &'a InvoiceRequest, payment_paths: Vec, - created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey, + invoice_request: &'a InvoiceRequest, custom_amount_msat: Option, + payment_paths: Vec, created_at: Duration, + payment_hash: PaymentHash, signing_pubkey: PublicKey, ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + let amount_msats = Self::amount_msats(invoice_request, custom_amount_msat)?; let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -314,10 +325,11 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_offer_using_keys( - invoice_request: &'a InvoiceRequest, payment_paths: Vec, - created_at: Duration, payment_hash: PaymentHash, keys: Keypair, + invoice_request: &'a InvoiceRequest, custom_amount_msat: Option, + payment_paths: Vec, created_at: Duration, + payment_hash: PaymentHash, keys: Keypair, ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + let amount_msats = Self::amount_msats(invoice_request, custom_amount_msat)?; let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), @@ -394,18 +406,43 @@ macro_rules! invoice_builder_methods { $self: ident, $self_type: ty, $return_type: ty, $return_value: expr, $type_param: ty $(, $self_mut: tt)? ) => { pub(crate) fn amount_msats( - invoice_request: &InvoiceRequest, + invoice_request: &InvoiceRequest, custom_amount: Option, ) -> Result { - match invoice_request.contents.inner.amount_msats() { - Some(amount_msats) => Ok(amount_msats), - None => match invoice_request.contents.inner.offer.amount() { - Some(Amount::Bitcoin { amount_msats }) => amount_msats - .checked_mul(invoice_request.quantity().unwrap_or(1)) - .ok_or(Bolt12SemanticError::InvalidAmount), - Some(Amount::Currency { .. }) => Err(Bolt12SemanticError::UnsupportedCurrency), - None => Err(Bolt12SemanticError::MissingAmount), - }, + let contents = &invoice_request.contents.inner; + + // Case 1: InvoiceRequest has an explicit amount. + if let Some(amount_msats) = contents.amount_msats() { + if let Some(custom) = custom_amount { + if custom != amount_msats { + return Err(Bolt12SemanticError::UnexpectedAmount); + } + } + return Ok(amount_msats); + } + + // Case 2: Offer has a Bitcoin amount. + if let Some(Amount::Bitcoin { amount_msats }) = contents.offer.amount() { + let total = amount_msats + .checked_mul(invoice_request.quantity().unwrap_or(1)) + .ok_or(Bolt12SemanticError::InvalidAmount)?; + + if let Some(custom) = custom_amount { + if custom < total { + return Err(Bolt12SemanticError::UnexpectedAmount); + } + return Ok(custom); + } + + return Ok(total); } + + // Case 3: Offer has a Currency amount. + if let Some(Amount::Currency { .. }) = contents.offer.amount() { + return custom_amount.ok_or(Bolt12SemanticError::MissingAmount); + } + + // Case 4: No amount provided anywhere. + Err(Bolt12SemanticError::MissingAmount) } #[cfg_attr(c_bindings, allow(dead_code))] @@ -736,6 +773,43 @@ pub struct Bolt12Invoice { tagged_hash: TaggedHash, } +impl Bolt12Invoice { + pub(crate) fn amount_source(&self) -> Result { + match &self.contents { + InvoiceContents::ForOffer { invoice_request, .. } => { + let amount = self.amount_msats(); + let ir_amount_source = invoice_request.amount_source()?; + + let expected_amount = match &ir_amount_source { + InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount { + invoice_request_amount_msats, + .. + } => Some(*invoice_request_amount_msats), + InvoiceRequestAmountSource::InvoiceRequestOnly { amount_msats } => { + Some(*amount_msats) + }, + InvoiceRequestAmountSource::OfferOnly { + amount: Amount::Bitcoin { amount_msats }, + } => Some(*amount_msats), + _ => None, + }; + + if let Some(expected) = expected_amount { + if amount != expected { + return Err(Bolt12SemanticError::UnexpectedAmount); + } + } + + Ok(Bolt12InvoiceAmountSource::Offer(ir_amount_source)) + }, + + InvoiceContents::ForRefund { refund, .. } => { + Ok(Bolt12InvoiceAmountSource::Refund(refund.amount_msats())) + }, + } + } +} + /// The contents of an [`Bolt12Invoice`] for responding to either an [`Offer`] or a [`Refund`]. /// /// [`Offer`]: crate::offers::offer::Offer @@ -768,6 +842,17 @@ struct InvoiceFields { experimental_baz: Option, } +/// The source of the amount for a [`Bolt12Invoice`]. +pub enum Bolt12InvoiceAmountSource { + /// The invoice corresponds to an [`Offer`] flow with the exact nature of amount specified + /// by [`InvoiceRequestAmountSource`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + Offer(InvoiceRequestAmountSource), + /// The invoice corresponds to a [`Refund`] flow, with the amount specified in the [`Refund`]. + Refund(u64), +} + macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// The chains that may be used when paying a requested invoice. /// @@ -1848,7 +1933,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths.clone(), payment_hash, now) + .respond_with_no_std(None, payment_paths.clone(), payment_hash, now) .unwrap() .build() .unwrap(); @@ -2211,7 +2296,7 @@ mod tests { .clone() .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx) .unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) + .respond_using_derived_keys_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build_and_sign(&secp_ctx) { @@ -2238,7 +2323,7 @@ mod tests { match invoice_request .verify_using_metadata(&expanded_key, &secp_ctx) .unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) + .respond_using_derived_keys_no_std(None, payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidMetadata), @@ -2328,7 +2413,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now) + .respond_with_no_std(None, payment_paths(), payment_hash(), now) .unwrap() .relative_expiry(one_hour.as_secs() as u32) .build() @@ -2349,7 +2434,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour) + .respond_with_no_std(None, payment_paths(), payment_hash(), now - one_hour) .unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build() @@ -2381,7 +2466,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2411,7 +2496,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2431,7 +2516,7 @@ mod tests { .quantity(u64::max_value()) .unwrap() .build_unchecked_and_sign() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), @@ -2459,7 +2544,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -2515,7 +2600,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2543,7 +2628,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2561,7 +2646,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2588,7 +2673,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2665,7 +2750,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2709,7 +2794,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .relative_expiry(3600) .build() @@ -2742,7 +2827,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2786,7 +2871,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2828,7 +2913,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2871,11 +2956,13 @@ mod tests { .build_and_sign() .unwrap(); #[cfg(not(c_bindings))] - let invoice_builder = - invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + let invoice_builder = invoice_request + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) + .unwrap(); #[cfg(c_bindings)] - let mut invoice_builder = - invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + let mut invoice_builder = invoice_request + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) + .unwrap(); let invoice_builder = invoice_builder .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -2934,7 +3021,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3022,6 +3109,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + None, payment_paths(), payment_hash(), now(), @@ -3052,6 +3140,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + None, payment_paths(), payment_hash(), now(), @@ -3093,7 +3182,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3122,7 +3211,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3186,7 +3275,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3219,7 +3308,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3262,7 +3351,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3301,7 +3390,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3347,7 +3436,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -3373,7 +3462,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3414,7 +3503,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3452,7 +3541,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3493,7 +3582,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3528,7 +3617,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 2e399738bae..a106e01c444 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -588,6 +588,36 @@ pub struct InvoiceRequest { signature: Signature, } +impl InvoiceRequestContents { + pub(crate) fn amount_source(&self) -> Result { + let ir_amount = self.amount_msats(); + let offer_amount = self.inner.offer.amount(); + + match (ir_amount, offer_amount) { + (Some(ir), Some(offer)) => { + if let Amount::Bitcoin { amount_msats } = offer { + if ir < amount_msats { + return Err(Bolt12SemanticError::InsufficientAmount); + } + } + + Ok(InvoiceRequestAmountSource::InvoiceRequestAndOfferAmount { + invoice_request_amount_msats: ir, + offer_amount: offer, + }) + }, + + (Some(amount_msats), None) => { + Ok(InvoiceRequestAmountSource::InvoiceRequestOnly { amount_msats }) + }, + + (None, Some(amount)) => Ok(InvoiceRequestAmountSource::OfferOnly { amount }), + + (None, None) => Err(Bolt12SemanticError::MissingAmount), + } + } +} + /// An [`InvoiceRequest`] that has been verified by [`InvoiceRequest::verify_using_metadata`] or /// [`InvoiceRequest::verify_using_recipient_data`] and exposes different ways to respond depending /// on whether the signing keys were derived. @@ -640,6 +670,29 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey { experimental_bar: Option, } +/// Represents the nature of amount specified with an [`InvoiceRequest`]. +pub enum InvoiceRequestAmountSource { + /// Both the [`InvoiceRequest`] amount and the corresponding Offer amount are set. + InvoiceRequestAndOfferAmount { + /// The amount in msats specified in the [`InvoiceRequest`]. + invoice_request_amount_msats: u64, + /// The corresponding Offer amount. + offer_amount: Amount, + }, + + /// Only the [`InvoiceRequest`] amount is set; the corresponding Offer amount is not specified. + InvoiceRequestOnly { + /// The amount in msats specified in the [`InvoiceRequest`]. + amount_msats: u64, + }, + + /// Only the corresponding Offer amount is set; the [`InvoiceRequest`] amount is not specified. + OfferOnly { + /// The corresponding Offer amount. + amount: Amount, + }, +} + macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { /// An unpredictable series of bytes, typically containing information about the derivation of /// [`payer_signing_pubkey`]. @@ -722,7 +775,7 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - $contents.respond_with_no_std(payment_paths, payment_hash, created_at) + $contents.respond_with_no_std(None, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request with the given required fields. @@ -751,7 +804,7 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( /// [`Bolt12Invoice::created_at`]: crate::offers::invoice::Bolt12Invoice::created_at /// [`OfferBuilder::deriving_signing_pubkey`]: crate::offers::offer::OfferBuilder::deriving_signing_pubkey pub fn respond_with_no_std( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + &$self, custom_amount_msat: Option, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration ) -> Result<$builder, Bolt12SemanticError> { if $contents.invoice_request_features().requires_unknown_bits() { @@ -763,13 +816,13 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( None => return Err(Bolt12SemanticError::MissingIssuerSigningPubkey), }; - <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) + <$builder>::for_offer(&$contents, custom_amount_msat, payment_paths, created_at, payment_hash, signing_pubkey) } #[cfg(test)] #[allow(dead_code)] pub(super) fn respond_with_no_std_using_signing_pubkey( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + &$self, custom_amount_msat: Option, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration, signing_pubkey: PublicKey ) -> Result<$builder, Bolt12SemanticError> { debug_assert!($contents.contents.inner.offer.issuer_signing_pubkey().is_none()); @@ -778,7 +831,7 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) + <$builder>::for_offer(&$contents, custom_amount_msat, payment_paths, created_at, payment_hash, signing_pubkey) } } } @@ -921,13 +974,13 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice #[cfg(feature = "std")] pub fn respond_using_derived_keys( - &$self, payment_paths: Vec, payment_hash: PaymentHash + &$self, amount_msats: Option, payment_paths: Vec, payment_hash: PaymentHash ) -> Result<$builder, Bolt12SemanticError> { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - $self.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at) + $self.respond_using_derived_keys_no_std(amount_msats, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses @@ -938,7 +991,7 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub fn respond_using_derived_keys_no_std( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + &$self, custom_amount_msats: Option, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration ) -> Result<$builder, Bolt12SemanticError> { if $self.inner.invoice_request_features().requires_unknown_bits() { @@ -956,7 +1009,7 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( } <$builder>::for_offer_using_keys( - &$self.inner, payment_paths, created_at, payment_hash, keys + &$self.inner, custom_amount_msats, payment_paths, created_at, payment_hash, keys ) } } } @@ -1631,7 +1684,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -2212,7 +2265,7 @@ mod tests { .features_unchecked(InvoiceRequestFeatures::unknown()) .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(None, payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnknownRequiredFeatures),