From 0026aaa0f5ca4c9ecffc7ea9bbcdd3c547a204c2 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 12 Feb 2026 19:12:53 +0530 Subject: [PATCH 1/8] [feat] Introduce the CurrencyConversion trait Add a `CurrencyConversion` trait for resolving currency-denominated amounts into millisatoshis. LDK cannot supply exchange rates itself, so applications provide this conversion logic as the foundation for fiat-denominated offer support. Co-Authored-By: OpenAI Codex --- fuzz/src/invoice_request_deser.rs | 11 ++++++- lightning/src/offers/currency.rs | 49 +++++++++++++++++++++++++++++++ lightning/src/offers/mod.rs | 2 ++ lightning/src/offers/offer.rs | 23 +++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lightning/src/offers/currency.rs diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index a21303debd7..7519cf5310a 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -16,9 +16,10 @@ use lightning::blinded_path::payment::{ }; use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; use lightning::ln::inbound_payment::ExpandedKey; +use lightning::offers::currency::CurrencyConversion; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; -use lightning::offers::offer::OfferId; +use lightning::offers::offer::{CurrencyCode, OfferId}; use lightning::offers::parse::Bolt12SemanticError; use lightning::sign::{EntropySource, ReceiveAuthKey}; use lightning::types::features::BlindedHopFeatures; @@ -61,6 +62,14 @@ pub fn do_test(data: &[u8], _out: Out) { } } +struct FuzzCurrencyConversion; + +impl CurrencyConversion for FuzzCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + Err(()) + } +} + struct Randomness; impl EntropySource for Randomness { diff --git a/lightning/src/offers/currency.rs b/lightning/src/offers/currency.rs new file mode 100644 index 00000000000..e2b983732e8 --- /dev/null +++ b/lightning/src/offers/currency.rs @@ -0,0 +1,49 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for currency conversion support. + +use crate::offers::offer::CurrencyCode; + +#[allow(unused_imports)] +use crate::prelude::*; +use core::ops::Deref; + +/// A trait for converting fiat currencies into millisatoshis (msats). +/// +/// Implementations must return the conversion rate in **msats per minor unit** +/// of the currency. For example: +/// +/// USD (exponent 2) → per **cent** (0.01 USD), not per dollar. +/// +/// The returned tolerance percent is currently unused by LDK's offer flows, but +/// remains part of the interface for callers that want to surface or preserve +/// that metadata alongside the conversion factor. +pub trait CurrencyConversion { + /// Returns the conversion rate in **msats per minor unit** for the given + /// ISO-4217 currency code together with an application-defined tolerance, + /// expressed as a percentage. + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result<(f64, u8), ()>; +} + +impl> CurrencyConversion for CC { + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + self.deref().msats_per_minor_unit(iso4217_code) + } +} + +/// A [`CurrencyConversion`] implementation that does not support +/// any fiat currency conversions. +pub struct DefaultCurrencyConversion; + +impl CurrencyConversion for DefaultCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + Err(()) + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 5b5cf6cdc78..608c017446f 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -16,6 +16,8 @@ pub mod offer; pub mod flow; +pub mod currency; + pub mod async_receive_offer_cache; pub mod invoice; pub mod invoice_error; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index b2703454169..272697af9b8 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -82,6 +82,7 @@ use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::currency::CurrencyConversion; use crate::offers::merkle::{TaggedHash, TlvRecord, TlvStream}; use crate::offers::nonce::Nonce; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; @@ -1125,6 +1126,28 @@ pub enum Amount { }, } +impl Amount { + pub(crate) fn into_msats( + self, currency_conversion: &CC, + ) -> Result { + match self { + Amount::Bitcoin { amount_msats } => Ok(amount_msats), + Amount::Currency { iso4217_code, amount } => { + let (msats_per_minor_unit, _) = currency_conversion + .msats_per_minor_unit(iso4217_code) + .map_err(|_| Bolt12SemanticError::UnsupportedCurrency)?; + let amount_msats = libm::round(msats_per_minor_unit * amount as f64); + + if !amount_msats.is_finite() { + return Err(Bolt12SemanticError::InvalidAmount); + } + + u64::try_from(amount_msats as i128).map_err(|_| Bolt12SemanticError::InvalidAmount) + }, + } + } +} + /// An ISO 4217 three-letter currency code (e.g., USD). /// /// Currency codes must be exactly 3 ASCII uppercase letters. From aafd2593827049de5e6fd7025742b71d36305c4c Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 2 Apr 2026 20:08:02 +0530 Subject: [PATCH 2/8] [test] Add CurrencyConversion amount conversion coverage Exercise the core currency-to-msat conversion helper directly. Cover supported conversions, unsupported currencies, and non-finite conversion outputs so the new trait has focused unit coverage. Co-Authored-By: OpenAI Codex --- lightning/src/offers/offer.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 272697af9b8..f43084996c6 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1418,6 +1418,7 @@ mod tests { use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::currency::CurrencyConversion; use crate::offers::nonce::Nonce; use crate::offers::offer::CurrencyCode; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; @@ -1431,6 +1432,14 @@ mod tests { use core::num::NonZeroU64; use core::time::Duration; + struct InfiniteCurrencyConversion; + + impl CurrencyConversion for InfiniteCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + Ok((f64::INFINITY, 0)) + } + } + #[test] fn builds_offer_with_defaults() { let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); @@ -1734,6 +1743,26 @@ mod tests { } } + #[test] + fn resolves_currency_amounts_into_msats() { + let conversion = TestCurrencyConversion; + let unsupported_conversion = InfiniteCurrencyConversion; + let supported_amount = + Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }; + let unsupported_amount = + Amount::Currency { iso4217_code: CurrencyCode::new(*b"EUR").unwrap(), amount: 10 }; + + assert_eq!(supported_amount.into_msats(&conversion), Ok(10_000)); + assert_eq!( + unsupported_amount.into_msats(&conversion), + Err(Bolt12SemanticError::UnsupportedCurrency) + ); + assert_eq!( + supported_amount.into_msats(&unsupported_conversion), + Err(Bolt12SemanticError::InvalidAmount) + ); + } + #[test] fn builds_offer_with_description() { let offer = OfferBuilder::new(pubkey(42)).description("foo".into()).build().unwrap(); From 7772153af79ab2c99a8581bc5ead30d47882b30b Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 6 Mar 2026 20:40:44 +0530 Subject: [PATCH 3/8] [feat] Keep CurrencyConversion in ChannelManager Store the `CurrencyConversion` implementation on `ChannelManager`. This keeps the conversion dependency owned by the higher-level payment state machine while leaving `OffersMessageFlow` focused on message coordination and path construction. Co-Authored-By: OpenAI Codex --- fuzz/src/full_stack.rs | 14 +++ lightning-background-processor/src/lib.rs | 8 ++ lightning-block-sync/src/init.rs | 6 +- lightning/src/ln/channelmanager.rs | 117 +++++++++++++++------- lightning/src/ln/functional_test_utils.rs | 13 +++ lightning/src/ln/functional_tests.rs | 2 + lightning/src/ln/reload_tests.rs | 6 +- lightning/src/offers/flow.rs | 1 + lightning/src/util/test_utils.rs | 14 +++ 9 files changed, 142 insertions(+), 39 deletions(-) diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index c1d7982e5e4..93c5ba8cfe6 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -51,7 +51,9 @@ use lightning::ln::peer_handler::{ }; use lightning::ln::script::ShutdownScript; use lightning::ln::types::ChannelId; +use lightning::offers::currency::CurrencyConversion; use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::offer::CurrencyCode; use lightning::onion_message::messenger::{Destination, MessageRouter, OnionMessagePath}; use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{ @@ -184,6 +186,14 @@ impl MessageRouter for FuzzRouter { } } +struct FuzzCurrencyConversion; + +impl CurrencyConversion for FuzzCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + Err(()) + } +} + struct TestBroadcaster { txn_broadcasted: Mutex>, } @@ -239,6 +249,7 @@ type ChannelMan<'a> = ChannelManager< Arc, &'a FuzzRouter, &'a FuzzRouter, + &'a FuzzCurrencyConversion, Arc, >; type PeerMan<'a> = PeerManager< @@ -549,6 +560,8 @@ pub fn do_test(mut data: &[u8], logger: &Arc let fee_est = Arc::new(FuzzEstimator { input: input.clone() }); let router = FuzzRouter {}; + let conversion = FuzzCurrencyConversion; + macro_rules! get_slice { ($len: expr) => { match input.get_slice($len as usize) { @@ -613,6 +626,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc broadcast.clone(), &router, &router, + &conversion, Arc::clone(&logger), keys_manager.clone(), keys_manager.clone(), diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index c796c53a031..e7bfeef3933 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -378,6 +378,9 @@ type DynMessageRouter = lightning::onion_message::messenger::DefaultMessageRoute &'static (dyn EntropySource + Send + Sync), >; +#[cfg(not(c_bindings))] +type DynCurrencyConversion = lightning::offers::currency::DefaultCurrencyConversion; + #[cfg(not(c_bindings))] type DynSignerProvider = dyn lightning::sign::SignerProvider + Send @@ -393,6 +396,7 @@ type DynChannelManager = lightning::ln::channelmanager::ChannelManager< &'static (dyn FeeEstimator + Send + Sync), &'static DynRouter, &'static DynMessageRouter, + &'static DynCurrencyConversion, &'static (dyn Logger + Send + Sync), >; @@ -1949,6 +1953,7 @@ mod tests { IgnoringMessageHandler, MessageHandler, PeerManager, SocketDescriptor, }; use lightning::ln::types::ChannelId; + use lightning::offers::currency::DefaultCurrencyConversion; use lightning::onion_message::messenger::{DefaultMessageRouter, OnionMessenger}; use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path, RouteHop}; @@ -2044,6 +2049,7 @@ mod tests { Arc, >, >, + Arc, Arc, >; @@ -2468,6 +2474,7 @@ mod tests { Arc::clone(&network_graph), Arc::clone(&keys_manager), )); + let conversion = Arc::new(DefaultCurrencyConversion); let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); let kv_store = Arc::new(Persister::new(format!("{}_persister_{}", &persist_dir, i).into())); @@ -2494,6 +2501,7 @@ mod tests { Arc::clone(&tx_broadcaster), Arc::clone(&router), Arc::clone(&msg_router), + Arc::clone(&conversion), Arc::clone(&logger), Arc::clone(&keys_manager), Arc::clone(&keys_manager), diff --git a/lightning-block-sync/src/init.rs b/lightning-block-sync/src/init.rs index 07c9f230be3..c8bda39889d 100644 --- a/lightning-block-sync/src/init.rs +++ b/lightning-block-sync/src/init.rs @@ -53,6 +53,7 @@ where /// use lightning::chain::chaininterface::BroadcasterInterface; /// use lightning::chain::chaininterface::FeeEstimator; /// use lightning::ln::channelmanager::{ChannelManager, ChannelManagerReadArgs}; +/// use lightning::offers::currency::CurrencyConversion; /// use lightning::onion_message::messenger::MessageRouter; /// use lightning::routing::router::Router; /// use lightning::sign; @@ -74,6 +75,7 @@ where /// F: FeeEstimator, /// R: Router, /// MR: MessageRouter, +/// CC: CurrencyConversion, /// L: Logger, /// C: chain::Filter, /// P: chainmonitor::Persist, @@ -88,6 +90,7 @@ where /// fee_estimator: &F, /// router: &R, /// message_router: &MR, +/// currency_conversion: &CC, /// logger: &L, /// persister: &P, /// ) { @@ -108,11 +111,12 @@ where /// tx_broadcaster, /// router, /// message_router, +/// currency_conversion, /// logger, /// config, /// vec![&mut monitor], /// ); -/// <(BestBlock, ChannelManager<&ChainMonitor, &T, &ES, &NS, &SP, &F, &R, &MR, &L>)>::read( +/// <(BestBlock, ChannelManager<&ChainMonitor, &T, &ES, &NS, &SP, &F, &R, &MR, &CC, &L>)>::read( /// &mut Cursor::new(&serialized_manager), read_args).unwrap() /// }; /// diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 660875400c7..3e18fa8f707 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -95,6 +95,9 @@ use crate::ln::outbound_payment::{ }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; +use crate::offers::currency::CurrencyConversion; +#[cfg(not(c_bindings))] +use crate::offers::currency::DefaultCurrencyConversion; use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow}; use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; @@ -1880,6 +1883,7 @@ pub type SimpleArcChannelManager = ChannelManager< >, >, Arc>>, Arc, Arc>>, + Arc, Arc, >; @@ -1911,6 +1915,7 @@ pub type SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L> ProbabilisticScorer<&'f NetworkGraph<&'g L>, &'g L>, >, &'i DefaultMessageRouter<&'f NetworkGraph<&'g L>, &'g L, &'c KeysManager>, + &'i DefaultCurrencyConversion, &'g L, >; @@ -1937,6 +1942,8 @@ pub trait AChannelManager { type Router: Router; /// A type implementing [`MessageRouter`]. type MessageRouter: MessageRouter; + /// A type implementing [`CurrencyConversion`]. + type CurrencyConversion: CurrencyConversion; /// A type implementing [`Logger`]. type Logger: Logger; /// Returns a reference to the actual [`ChannelManager`] object. @@ -1951,6 +1958,7 @@ pub trait AChannelManager { Self::FeeEstimator, Self::Router, Self::MessageRouter, + Self::CurrencyConversion, Self::Logger, >; } @@ -1964,8 +1972,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > AChannelManager for ChannelManager + > AChannelManager for ChannelManager { type Watch = M; type Broadcaster = T; @@ -1976,8 +1985,9 @@ impl< type FeeEstimator = F; type Router = R; type MessageRouter = MR; + type CurrencyConversion = CC; type Logger = L; - fn get_cm(&self) -> &ChannelManager { + fn get_cm(&self) -> &ChannelManager { self } } @@ -2057,6 +2067,7 @@ impl< /// # tx_broadcaster: &dyn lightning::chain::chaininterface::BroadcasterInterface, /// # router: &lightning::routing::router::DefaultRouter<&NetworkGraph<&'a L>, &'a L, &ES, &S, SP, SL>, /// # message_router: &lightning::onion_message::messenger::DefaultMessageRouter<&NetworkGraph<&'a L>, &'a L, &ES>, +/// # currency_conversion: &lightning::offers::currency::DefaultCurrencyConversion, /// # logger: &L, /// # entropy_source: &ES, /// # node_signer: &dyn lightning::sign::NodeSigner, @@ -2072,18 +2083,18 @@ impl< /// }; /// let config = UserConfig::default(); /// let channel_manager = ChannelManager::new( -/// fee_estimator, chain_monitor, tx_broadcaster, router, message_router, logger, -/// entropy_source, node_signer, signer_provider, config.clone(), params, current_timestamp, +/// fee_estimator, chain_monitor, tx_broadcaster, router, message_router, currency_conversion, +/// logger, entropy_source, node_signer, signer_provider, config.clone(), params, current_timestamp, /// ); /// /// // Restart from deserialized data /// let mut channel_monitors = read_channel_monitors(); /// let args = ChannelManagerReadArgs::new( /// entropy_source, node_signer, signer_provider, fee_estimator, chain_monitor, tx_broadcaster, -/// router, message_router, logger, config, channel_monitors.iter().collect(), +/// router, message_router, currency_conversion, logger, config, channel_monitors.iter().collect(), /// ); /// let (best_block, channel_manager) = -/// <(BestBlock, ChannelManager<_, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; +/// <(BestBlock, ChannelManager<_, _, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; /// /// // Update the ChannelManager and ChannelMonitors with the latest chain data /// // ... @@ -2729,6 +2740,7 @@ pub struct ChannelManager< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, > { config: RwLock, @@ -2742,6 +2754,10 @@ pub struct ChannelManager< pub(super) flow: OffersMessageFlow, #[cfg(not(test))] flow: OffersMessageFlow, + #[cfg(test)] + pub(super) currency_conversion: CC, + #[cfg(not(test))] + currency_conversion: CC, #[cfg(any(test, feature = "_test_utils"))] pub(super) best_block: RwLock, @@ -3563,8 +3579,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > ChannelManager + > ChannelManager { /// Constructs a new `ChannelManager` to hold several channels and route between them. /// @@ -3585,9 +3602,9 @@ impl< /// [`params.best_block.block_hash`]: chain::BestBlock::block_hash #[rustfmt::skip] pub fn new( - fee_est: F, chain_monitor: M, tx_broadcaster: T, router: R, message_router: MR, logger: L, - entropy_source: ES, node_signer: NS, signer_provider: SP, config: UserConfig, - params: ChainParameters, current_timestamp: u32, + fee_est: F, chain_monitor: M, tx_broadcaster: T, router: R, message_router: MR, + currency_conversion: CC, logger: L, entropy_source: ES, node_signer: NS, + signer_provider: SP, config: UserConfig, params: ChainParameters, current_timestamp: u32, ) -> Self where L: Clone, @@ -3601,7 +3618,8 @@ impl< let flow = OffersMessageFlow::new( ChainHash::using_genesis_block(params.network), params.best_block, our_network_pubkey, current_timestamp, expanded_inbound_key, - node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router, logger.clone(), + node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router, + logger.clone(), ); ChannelManager { @@ -3612,6 +3630,7 @@ impl< tx_broadcaster, router, flow, + currency_conversion, best_block: RwLock::new(params.best_block), @@ -14488,8 +14507,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > ChannelManager + > ChannelManager { #[cfg(not(c_bindings))] create_offer_builder!(self, OfferBuilder<'_, DerivedMetadata, secp256k1::All>); @@ -15335,8 +15355,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > BaseMessageHandler for ChannelManager + > BaseMessageHandler for ChannelManager { fn provided_node_features(&self) -> NodeFeatures { provided_node_features(&self.config.read().unwrap()) @@ -15721,8 +15742,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > EventsProvider for ChannelManager + > EventsProvider for ChannelManager { /// Processes events that must be periodically handled. /// @@ -15746,8 +15768,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > chain::Listen for ChannelManager + > chain::Listen for ChannelManager { fn filtered_block_connected(&self, header: &Header, txdata: &TransactionData, height: u32) { { @@ -15797,8 +15820,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > chain::Confirm for ChannelManager + > chain::Confirm for ChannelManager { #[rustfmt::skip] fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { @@ -15960,8 +15984,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > ChannelManager + > ChannelManager { /// Calls a function which handles an on-chain event (blocks dis/connected, transactions /// un/confirmed, etc) on each channel, handling any resulting errors or messages generated by @@ -16314,8 +16339,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > ChannelMessageHandler for ChannelManager + > ChannelMessageHandler for ChannelManager { fn handle_open_channel(&self, counterparty_node_id: PublicKey, message: &msgs::OpenChannel) { // Note that we never need to persist the updated ChannelManager for an inbound @@ -16874,8 +16900,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > OffersMessageHandler for ChannelManager + > OffersMessageHandler for ChannelManager { #[rustfmt::skip] fn handle_message( @@ -17082,8 +17109,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > AsyncPaymentsMessageHandler for ChannelManager + > AsyncPaymentsMessageHandler for ChannelManager { fn handle_offer_paths_request( &self, message: OfferPathsRequest, context: AsyncPaymentsContext, @@ -17328,8 +17356,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > NodeIdLookUp for ChannelManager + > NodeIdLookUp for ChannelManager { fn next_node_id(&self, short_channel_id: u64) -> Option { self.short_to_chan_info.read().unwrap().get(&short_channel_id).map(|(pubkey, _)| *pubkey) @@ -17853,8 +17882,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > Writeable for ChannelManager + > Writeable for ChannelManager { #[rustfmt::skip] fn write(&self, writer: &mut W) -> Result<(), io::Error> { @@ -18588,6 +18618,7 @@ pub struct ChannelManagerReadArgs< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, > { /// A cryptographically secure source of entropy. @@ -18626,6 +18657,11 @@ pub struct ChannelManagerReadArgs< /// /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath pub message_router: MR, + /// The [`CurrencyConversion`] used for supporting and interpreting [`Offer`] amount + /// denoted in [`Amount::Currency`]. + /// + /// [`Amount::Currency`]: crate::offers::offer::Amount::Currency + pub currency_conversion: CC, /// The Logger for use in the ChannelManager and which may be used to log information during /// deserialization. pub logger: L, @@ -18666,16 +18702,18 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L> + > ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, CC, L> { /// Simple utility function to create a ChannelManagerReadArgs which creates the monitor /// HashMap for you. This is primarily useful for C bindings where it is not practical to /// populate a HashMap directly from C. pub fn new( entropy_source: ES, node_signer: NS, signer_provider: SP, fee_estimator: F, - chain_monitor: M, tx_broadcaster: T, router: R, message_router: MR, logger: L, - config: UserConfig, mut channel_monitors: Vec<&'a ChannelMonitor>, + chain_monitor: M, tx_broadcaster: T, router: R, message_router: MR, + currency_conversion: CC, logger: L, config: UserConfig, + mut channel_monitors: Vec<&'a ChannelMonitor>, ) -> Self { Self { entropy_source, @@ -18686,6 +18724,7 @@ impl< tx_broadcaster, router, message_router, + currency_conversion, logger, config, channel_monitors: hash_map_from_iter( @@ -18743,15 +18782,16 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ReadableArgs> - for (BestBlock, Arc>) + > ReadableArgs> + for (BestBlock, Arc>) { fn read( - reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>, + reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, CC, L>, ) -> Result { let (best_block, chan_manager) = - <(BestBlock, ChannelManager)>::read(reader, args)?; + <(BestBlock, ChannelManager)>::read(reader, args)?; Ok((best_block, Arc::new(chan_manager))) } } @@ -18766,12 +18806,13 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ReadableArgs> - for (BestBlock, ChannelManager) + > ReadableArgs> + for (BestBlock, ChannelManager) { fn read( - reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>, + reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, CC, L>, ) -> Result { // Stage 1: Pure deserialization into DTO let data: ChannelManagerData = ChannelManagerData::read( @@ -18798,8 +18839,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ChannelManager + > ChannelManager { /// Constructs a `ChannelManager` from deserialized data and runtime dependencies. /// @@ -18811,7 +18853,7 @@ impl< /// [`ChannelMonitorUpdate`]s. pub(super) fn from_channel_manager_data( data: ChannelManagerData, - mut args: ChannelManagerReadArgs<'_, M, T, ES, NS, SP, F, R, MR, L>, + mut args: ChannelManagerReadArgs<'_, M, T, ES, NS, SP, F, R, MR, CC, L>, ) -> Result<(BestBlock, Self), DecodeError> { let ChannelManagerData { chain_hash, @@ -20043,6 +20085,7 @@ impl< tx_broadcaster: args.tx_broadcaster, router: args.router, flow, + currency_conversion: args.currency_conversion, best_block: RwLock::new(best_block), @@ -21657,6 +21700,7 @@ pub mod bench { &'a test_utils::TestFeeEstimator, &'a test_utils::TestRouter<'a>, &'a test_utils::TestMessageRouter<'a>, + &'a test_utils::TestCurrencyConversion, &'a test_utils::TestLogger, >; @@ -21695,6 +21739,7 @@ pub mod bench { let entropy = test_utils::TestKeysInterface::new(&[0u8; 32], network); let router = test_utils::TestRouter::new(Arc::new(NetworkGraph::new(network, &logger_a)), &logger_a, &scorer); let message_router = test_utils::TestMessageRouter::new_default(Arc::new(NetworkGraph::new(network, &logger_a)), &entropy); + let currency_conversion = test_utils::TestCurrencyConversion; let mut config: UserConfig = Default::default(); config.channel_config.max_dust_htlc_exposure = MaxDustHTLCExposure::FeeRateMultiplier(5_000_000 / 253); @@ -21703,7 +21748,7 @@ pub mod bench { let seed_a = [1u8; 32]; let keys_manager_a = KeysManager::new(&seed_a, 42, 42, true); let chain_monitor_a = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_a, &keys_manager_a, keys_manager_a.get_peer_storage_key(), false); - let node_a = ChannelManager::new(&fee_estimator, &chain_monitor_a, &tx_broadcaster, &router, &message_router, &logger_a, &keys_manager_a, &keys_manager_a, &keys_manager_a, config.clone(), ChainParameters { + let node_a = ChannelManager::new(&fee_estimator, &chain_monitor_a, &tx_broadcaster, &router, &message_router, ¤cy_conversion, &logger_a, &keys_manager_a, &keys_manager_a, &keys_manager_a, config.clone(), ChainParameters { network, best_block: BestBlock::from_network(network), }, genesis_block.header.time); @@ -21713,7 +21758,7 @@ pub mod bench { let seed_b = [2u8; 32]; let keys_manager_b = KeysManager::new(&seed_b, 42, 42, true); let chain_monitor_b = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_b, &keys_manager_b, keys_manager_b.get_peer_storage_key(), false); - let node_b = ChannelManager::new(&fee_estimator, &chain_monitor_b, &tx_broadcaster, &router, &message_router, &logger_b, &keys_manager_b, &keys_manager_b, &keys_manager_b, config.clone(), ChainParameters { + let node_b = ChannelManager::new(&fee_estimator, &chain_monitor_b, &tx_broadcaster, &router, &message_router, ¤cy_conversion, &logger_b, &keys_manager_b, &keys_manager_b, &keys_manager_b, config.clone(), ChainParameters { network, best_block: BestBlock::from_network(network), }, genesis_block.header.time); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index d39cee78b0f..67d5ace3d1d 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -501,6 +501,7 @@ pub struct NodeCfg<'a> { pub fee_estimator: &'a test_utils::TestFeeEstimator, pub router: test_utils::TestRouter<'a>, pub message_router: test_utils::TestMessageRouter<'a>, + pub currency_conversion: test_utils::TestCurrencyConversion, pub chain_monitor: test_utils::TestChainMonitor<'a>, pub keys_manager: &'a test_utils::TestKeysInterface, pub logger: &'a test_utils::TestLogger, @@ -518,6 +519,7 @@ pub type TestChannelManager<'node_cfg, 'chan_mon_cfg> = ChannelManager< &'chan_mon_cfg test_utils::TestFeeEstimator, &'node_cfg test_utils::TestRouter<'chan_mon_cfg>, &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, + &'node_cfg test_utils::TestCurrencyConversion, &'chan_mon_cfg test_utils::TestLogger, >; @@ -552,6 +554,7 @@ pub struct Node<'chan_man, 'node_cfg: 'chan_man, 'chan_mon_cfg: 'node_cfg> { pub fee_estimator: &'chan_mon_cfg test_utils::TestFeeEstimator, pub router: &'node_cfg test_utils::TestRouter<'chan_mon_cfg>, pub message_router: &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, + pub currency_conversion: &'node_cfg test_utils::TestCurrencyConversion, pub chain_monitor: &'node_cfg test_utils::TestChainMonitor<'chan_mon_cfg>, pub keys_manager: &'chan_mon_cfg test_utils::TestKeysInterface, pub node: &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, @@ -737,6 +740,7 @@ pub trait NodeHolder { ::FeeEstimator, ::Router, ::MessageRouter, + ::CurrencyConversion, ::Logger, >; fn chain_monitor(&self) -> Option<&test_utils::TestChainMonitor<'_>>; @@ -754,6 +758,7 @@ impl NodeHolder for &H { ::FeeEstimator, ::Router, ::MessageRouter, + ::CurrencyConversion, ::Logger, > { (*self).node() @@ -884,6 +889,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, + &test_utils::TestCurrencyConversion, &test_utils::TestLogger, >, )>::read( @@ -903,6 +909,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { network_graph, self.keys_manager, ), + currency_conversion: &test_utils::TestCurrencyConversion, chain_monitor: self.chain_monitor, tx_broadcaster: &broadcaster, logger: &self.logger, @@ -1334,6 +1341,7 @@ pub fn _reload_node<'a, 'b, 'c>( fee_estimator: node.fee_estimator, router: node.router, message_router: node.message_router, + currency_conversion: node.currency_conversion, chain_monitor: node.chain_monitor, tx_broadcaster: node.tx_broadcaster, logger: node.logger, @@ -4605,6 +4613,7 @@ where Arc::clone(&network_graph), &cfg.keys_manager, ), + currency_conversion: test_utils::TestCurrencyConversion, chain_monitor, keys_manager: &cfg.keys_manager, node_seed: seed, @@ -4706,6 +4715,7 @@ pub fn create_node_chanmgrs<'a, 'b>( &'b test_utils::TestFeeEstimator, &'a test_utils::TestRouter<'b>, &'a test_utils::TestMessageRouter<'b>, + &'a test_utils::TestCurrencyConversion, &'b test_utils::TestLogger, >, > { @@ -4720,6 +4730,7 @@ pub fn create_node_chanmgrs<'a, 'b>( cfgs[i].tx_broadcaster, &cfgs[i].router, &cfgs[i].message_router, + &cfgs[i].currency_conversion, cfgs[i].logger, cfgs[i].keys_manager, cfgs[i].keys_manager, @@ -4750,6 +4761,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>( &'c test_utils::TestFeeEstimator, &'c test_utils::TestRouter, &'c test_utils::TestMessageRouter, + &'c test_utils::TestCurrencyConversion, &'c test_utils::TestLogger, >, >, @@ -4805,6 +4817,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>( fee_estimator: cfgs[i].fee_estimator, router: &cfgs[i].router, message_router: &cfgs[i].message_router, + currency_conversion: &cfgs[i].currency_conversion, chain_monitor: &cfgs[i].chain_monitor, keys_manager: &cfgs[i].keys_manager, node: &chan_mgrs[i], diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 32a07be4d2b..bfd42bbe571 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -4869,6 +4869,7 @@ pub fn test_key_derivation_params() { test_utils::TestRouter::new(Arc::clone(&network_graph), &chanmon_cfgs[0].logger, &scorer); let message_router = test_utils::TestMessageRouter::new_default(Arc::clone(&network_graph), &keys_manager); + let currency_conversion = test_utils::TestCurrencyConversion {}; let node = NodeCfg { chain_source: &chanmon_cfgs[0].chain_source, logger: &chanmon_cfgs[0].logger, @@ -4876,6 +4877,7 @@ pub fn test_key_derivation_params() { fee_estimator: &chanmon_cfgs[0].fee_estimator, router, message_router, + currency_conversion, chain_monitor, keys_manager: &keys_manager, network_graph, diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index 9e992467ecd..91aaa911883 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -426,7 +426,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut nodes_0_read = &nodes_0_serialized[..]; if let Err(msgs::DecodeError::DangerousValue) = - <(BestBlock, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { + <(BestBlock, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestCurrencyConversion, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { config: UserConfig::default(), entropy_source: keys_manager, node_signer: keys_manager, @@ -434,6 +434,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { fee_estimator: &fee_estimator, router: &nodes[0].router, message_router: &nodes[0].message_router, + currency_conversion: &nodes[0].currency_conversion, chain_monitor: nodes[0].chain_monitor, tx_broadcaster: nodes[0].tx_broadcaster, logger: &logger, @@ -445,7 +446,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut nodes_0_read = &nodes_0_serialized[..]; let (_, nodes_0_deserialized_tmp) = - <(BestBlock, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { + <(BestBlock, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestCurrencyConversion, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { config: UserConfig::default(), entropy_source: keys_manager, node_signer: keys_manager, @@ -453,6 +454,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { fee_estimator: &fee_estimator, router: nodes[0].router, message_router: &nodes[0].message_router, + currency_conversion: &nodes[0].currency_conversion, chain_monitor: nodes[0].chain_monitor, tx_broadcaster: nodes[0].tx_broadcaster, logger: &logger, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index c1c3ce26aee..402a9211735 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -26,6 +26,7 @@ use crate::blinded_path::payment::{ }; use crate::chain::channelmonitor::LATENCY_GRACE_PERIOD_BLOCKS; +use crate::offers::currency::CurrencyConversion; #[allow(unused_imports)] use crate::prelude::*; diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 57f9ba6b22f..6a371f89b08 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -31,7 +31,9 @@ use crate::ln::msgs::{BaseMessageHandler, MessageSendEvent}; use crate::ln::script::ShutdownScript; use crate::ln::types::ChannelId; use crate::ln::{msgs, wire}; +use crate::offers::currency::CurrencyConversion; use crate::offers::invoice::UnsignedBolt12Invoice; +use crate::offers::offer::CurrencyCode; use crate::onion_message::messenger::{ DefaultMessageRouter, Destination, MessageRouter, NodeIdMessageRouter, OnionMessagePath, }; @@ -448,6 +450,18 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { } } +pub struct TestCurrencyConversion; + +impl CurrencyConversion for TestCurrencyConversion { + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result<(f64, u8), ()> { + if iso4217_code.as_str() == "USD" { + Ok((1_000.0, 0)) // 1 cent = 1000 msats (test-only fixed rate) + } else { + Err(()) + } + } +} + pub struct OnlyReadsKeysInterface {} impl EntropySource for OnlyReadsKeysInterface { From 4e4efccef0e2c5698f6a4348aeb46065c178f54f Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 2 Mar 2026 16:12:58 +0530 Subject: [PATCH 4/8] [feat] Support fiat-denominated amounts in OfferBuilder Allow `OfferBuilder` to set currency-denominated amounts when the caller provides a `CurrencyConversion` implementation. Move amount validation into the setters so fiat offers are checked when they are configured and `build()` no longer repeats that work. Co-Authored-By: OpenAI Codex --- lightning/src/ln/async_payments_tests.rs | 6 +- lightning/src/ln/channelmanager.rs | 4 +- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/ln/offers_tests.rs | 98 +++---- lightning/src/ln/outbound_payment.rs | 16 +- lightning/src/offers/flow.rs | 10 +- lightning/src/offers/invoice.rs | 74 +++--- lightning/src/offers/invoice_request.rs | 148 +++++------ lightning/src/offers/merkle.rs | 15 +- lightning/src/offers/offer.rs | 240 +++++++++--------- lightning/src/offers/static_invoice.rs | 45 ++-- lightning/src/offers/test_utils.rs | 3 +- 12 files changed, 318 insertions(+), 343 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 341bd5d7269..8ca3b747ef4 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -324,7 +324,7 @@ fn create_static_invoice( .flow .create_async_receive_offer_builder(entropy_source, blinded_paths_to_always_online_node) .unwrap(); - let offer = offer_builder.build().unwrap(); + let offer = offer_builder.build(); let static_invoice = create_static_invoice_builder(recipient, &offer, offer_nonce, relative_expiry) .build_and_sign(&secp_ctx) @@ -695,7 +695,7 @@ fn static_invoice_unknown_required_features() { .flow .create_async_receive_offer_builder(entropy_source, blinded_paths_to_always_online_node) .unwrap(); - let offer = offer_builder.build().unwrap(); + let offer = offer_builder.build(); let static_invoice_unknown_req_features = create_static_invoice_builder(&nodes[2], &offer, nonce, None) .features_unchecked(Bolt12InvoiceFeatures::unknown()) @@ -1679,7 +1679,7 @@ fn invalid_async_receive_with_retry( .flow .create_async_receive_offer_builder(entropy_source, blinded_paths_to_always_online_node) .unwrap(); - let offer = offer_builder.build().unwrap(); + let offer = offer_builder.build(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3e18fa8f707..dc902ae3911 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2454,8 +2454,8 @@ impl< /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); /// # let offer = builder /// .description("coffee".to_string()) -/// .amount_msats(10_000_000) -/// .build()?; +/// .amount_msats(10_000_000).unwrap() +/// .build(); /// let bech32_offer = offer.to_string(); /// /// // On the event processing thread diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 45640d3486d..e4455a13b52 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -519,7 +519,7 @@ fn bolt12_invoice_too_large_blinded_paths() { ), ]); - let offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); + let offer = nodes[1].node.create_offer_builder().unwrap().build(); let payment_id = PaymentId([1; 32]); nodes[0] .node diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index de08af5d276..6b19f4b783a 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -326,8 +326,8 @@ fn create_offer_with_no_blinded_path() { let router = NullMessageRouter {}; let offer = alice.node .create_offer_builder_using_router(&router).unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_eq!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(offer.paths().is_empty()); } @@ -402,8 +402,8 @@ fn prefers_non_tor_nodes_in_blinded_paths() { let offer = bob.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -418,8 +418,8 @@ fn prefers_non_tor_nodes_in_blinded_paths() { let offer = bob.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -469,8 +469,8 @@ fn prefers_more_connected_nodes_in_blinded_paths() { let offer = bob.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -504,8 +504,8 @@ fn check_dummy_hop_pattern_in_offer() { let compact_offer = alice.node .create_offer_builder_using_router(&default_router).unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert!(!compact_offer.paths().is_empty()); @@ -532,8 +532,8 @@ fn check_dummy_hop_pattern_in_offer() { let padded_offer = alice.node .create_offer_builder_using_router(&node_id_router).unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert!(!padded_offer.paths().is_empty()); assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == QR_CODED_DUMMY_HOPS_PATH_LENGTH)); @@ -565,7 +565,7 @@ fn creates_short_lived_offer() { let offer = alice.node .create_offer_builder().unwrap() - .build().unwrap(); + .build(); assert!(!offer.paths().is_empty()); for path in offer.paths() { let introduction_node_id = resolve_introduction_node(bob, &path); @@ -591,7 +591,7 @@ fn creates_long_lived_offer() { let offer = alice.node .create_offer_builder_using_router(&router) .unwrap() - .build().unwrap(); + .build(); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); @@ -697,8 +697,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { let offer = alice.node .create_offer_builder() .unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -862,8 +862,8 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -986,8 +986,8 @@ fn pays_for_offer_without_blinded_paths() { let offer = alice.node .create_offer_builder().unwrap() .clear_paths() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_eq!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(offer.paths().is_empty()); @@ -1111,8 +1111,8 @@ fn send_invoice_requests_with_distinct_reply_path() { let offer = alice.node .create_offer_builder() .unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -1245,8 +1245,8 @@ fn creates_and_pays_for_offer_with_retry() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -1321,8 +1321,8 @@ fn pays_bolt12_invoice_asynchronously() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); let payment_id = PaymentId([1; 32]); bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); @@ -1413,8 +1413,8 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -1557,8 +1557,8 @@ fn fails_authentication_when_handling_invoice_request() { let offer = alice.node .create_offer_builder() .unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_eq!(offer.metadata(), None); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); @@ -1569,7 +1569,7 @@ fn fails_authentication_when_handling_invoice_request() { let invalid_path = alice.node .create_offer_builder() .unwrap() - .build().unwrap() + .build() .paths().first().unwrap() .clone(); assert!(check_compact_path_introduction_node(&invalid_path, alice, bob_id)); @@ -1667,8 +1667,8 @@ fn fails_authentication_when_handling_invoice_for_offer() { let offer = alice.node .create_offer_builder() .unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -1871,9 +1871,9 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) + .amount_msats(10_000_000).unwrap() .absolute_expiry(absolute_expiry) - .build().unwrap(); + .build(); let payment_id = PaymentId([1; 32]); @@ -1973,7 +1973,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { .create_offer_builder().unwrap() .clear_chains() .chain(Network::Signet) - .build().unwrap(); + .build(); match bob.node.pay_for_offer(&offer, None, PaymentId([1; 32]), Default::default()) { Ok(_) => panic!("Expected error"), @@ -2029,8 +2029,8 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); match david.node.pay_for_offer(&offer, None, PaymentId([1; 32]), Default::default()) { Ok(_) => panic!("Expected error"), @@ -2061,8 +2061,8 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); let payment_id = PaymentId([1; 32]); assert!(david.node.pay_for_offer( &offer, None, payment_id, Default::default()).is_ok()); @@ -2143,8 +2143,8 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); let payment_id = PaymentId([1; 32]); david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); @@ -2351,8 +2351,8 @@ fn fails_paying_invoice_with_unknown_required_features() { let offer = alice.node .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); let payment_id = PaymentId([1; 32]); david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); @@ -2436,7 +2436,7 @@ fn rejects_keysend_to_non_static_invoice_path() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); // First pay the offer and save the payment preimage and invoice. - let offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); + let offer = nodes[1].node.create_offer_builder().unwrap().build(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); @@ -2519,8 +2519,8 @@ fn no_double_pay_with_stale_channelmanager() { let offer = nodes[1].node .create_offer_builder().unwrap() .clear_paths() - .amount_msats(amt_msat) - .build().unwrap(); + .amount_msats(amt_msat).unwrap() + .build(); assert_eq!(offer.issuer_signing_pubkey(), Some(bob_id)); assert!(offer.paths().is_empty()); @@ -2598,8 +2598,8 @@ fn creates_and_pays_for_phantom_offer() { let offer = nodes[1].node .create_phantom_offer_builder(vec![(node_c_id, nodes[2].node.list_channels())], 2) .unwrap() - .amount_msats(10_000_000) - .build().unwrap(); + .amount_msats(10_000_000).unwrap() + .build(); // The offer should be resolvable by either of node B or C but signed by a derived key assert!(offer.issuer_signing_pubkey().is_some()); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 7259f60796f..bf9132c665e 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -3256,8 +3256,8 @@ mod tests { let created_at = now() - DEFAULT_RELATIVE_EXPIRY; let invoice = OfferBuilder::new(recipient_pubkey()) - .amount_msats(1000) - .build().unwrap() + .amount_msats(1000).unwrap() + .build() .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() @@ -3305,8 +3305,8 @@ mod tests { let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); let invoice = OfferBuilder::new(recipient_pubkey()) - .amount_msats(1000) - .build().unwrap() + .amount_msats(1000).unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() @@ -3370,8 +3370,8 @@ mod tests { let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); let invoice = OfferBuilder::new(recipient_pubkey()) - .amount_msats(1000) - .build().unwrap() + .amount_msats(1000).unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() @@ -3459,8 +3459,8 @@ mod tests { let payment_id = PaymentId([1; 32]); OfferBuilder::new(recipient_pubkey()) - .amount_msats(1000) - .build().unwrap() + .amount_msats(1000).unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 402a9211735..36881427dc5 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1623,14 +1623,8 @@ impl OffersMessageFlow { offer_builder = offer_builder.absolute_expiry(Duration::from_secs(paths_absolute_expiry)); } - let (offer_id, offer) = match offer_builder.build() { - Ok(offer) => (offer.id(), offer), - Err(_) => { - log_error!(self.logger, "Failed to build async receive offer"); - debug_assert!(false); - return None; - }, - }; + let offer = offer_builder.build(); + let offer_id = offer.id(); let (invoice, forward_invoice_request_path) = match self.create_static_invoice_for_server( &offer, diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index fd77595ca7d..6e6e168e01a 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1898,8 +1898,8 @@ mod tests { let now = now(); let unsigned_invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2168,9 +2168,9 @@ mod tests { if let Err(e) = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .absolute_expiry(future_expiry) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2184,9 +2184,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .absolute_expiry(past_expiry) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked_and_sign() @@ -2254,10 +2254,10 @@ mod tests { let invoice_request = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) + .unwrap() .path(blinded_path) .experimental_foo(42) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2367,8 +2367,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2388,8 +2388,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2418,8 +2418,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1001) @@ -2447,9 +2447,9 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(2) @@ -2468,9 +2468,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(u64::max_value()) @@ -2498,8 +2498,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2554,8 +2554,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2582,8 +2582,8 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2600,8 +2600,8 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2627,8 +2627,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2704,8 +2704,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2748,8 +2748,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2781,8 +2781,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2825,8 +2825,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2867,8 +2867,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2909,8 +2909,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2973,8 +2973,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3058,10 +3058,10 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .clear_issuer_signing_pubkey() .amount_msats(1000) + .unwrap() .path(paths[0].clone()) .path(paths[1].clone()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3088,10 +3088,10 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .clear_issuer_signing_pubkey() .amount_msats(1000) + .unwrap() .path(paths[0].clone()) .path(paths[1].clone()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3132,8 +3132,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3159,8 +3159,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1000) @@ -3225,8 +3225,8 @@ mod tests { let mut buffer = Vec::new(); OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3258,8 +3258,8 @@ mod tests { let mut invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3301,8 +3301,8 @@ mod tests { let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3340,8 +3340,8 @@ mod tests { let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3386,8 +3386,8 @@ mod tests { let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3412,8 +3412,8 @@ mod tests { let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3453,8 +3453,8 @@ mod tests { let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3491,8 +3491,8 @@ mod tests { let invoice = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3532,8 +3532,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3567,8 +3567,8 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3610,7 +3610,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let offer = OfferBuilder::new(recipient_pubkey()).amount_msats(1000).build().unwrap(); + let offer = OfferBuilder::new(recipient_pubkey()).amount_msats(1000).unwrap().build(); let offer_id = offer.id(); @@ -3658,7 +3658,7 @@ mod tests { let now = Duration::from_secs(123456); let payment_id = PaymentId([1; 32]); - let offer = OfferBuilder::new(node_id).amount_msats(1000).build().unwrap(); + let offer = OfferBuilder::new(node_id).amount_msats(1000).unwrap().build(); let invoice_request = offer .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 7805882ef73..1ee1b71cff3 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -71,6 +71,7 @@ use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; +use crate::offers::currency::CurrencyConversion; use crate::offers::invoice::{DerivedSigningPubkey, ExplicitSigningPubkey, SigningPubkeyStrategy}; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, @@ -1573,6 +1574,7 @@ mod tests { use crate::types::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::types::string::{PrintableString, UntrustedString}; use crate::util::ser::{BigSize, Readable, Writeable}; + use crate::util::test_utils::TestCurrencyConversion; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1::{self, Keypair, Secp256k1, SecretKey}; @@ -1591,8 +1593,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -1683,9 +1685,9 @@ mod tests { if let Err(e) = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .absolute_expiry(future_expiry) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -1695,9 +1697,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .absolute_expiry(past_expiry) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -1717,9 +1719,9 @@ mod tests { let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .experimental_foo(42) - .build() - .unwrap(); + .build(); let invoice_request = offer .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() @@ -1830,8 +1832,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Bitcoin) @@ -1844,9 +1846,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .chain(Network::Testnet) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Testnet) @@ -1859,10 +1861,10 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .chain(Network::Bitcoin) .chain(Network::Testnet) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Bitcoin) @@ -1875,10 +1877,10 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .chain(Network::Bitcoin) .chain(Network::Testnet) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Bitcoin) @@ -1893,9 +1895,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .chain(Network::Testnet) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Bitcoin) @@ -1906,9 +1908,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .chain(Network::Testnet) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -1928,8 +1930,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1000) @@ -1943,8 +1945,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1001) @@ -1960,8 +1962,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1001) @@ -1975,8 +1977,8 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(999) @@ -1987,9 +1989,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(2) @@ -2002,8 +2004,8 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(MAX_VALUE_MSAT + 1) @@ -2014,9 +2016,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1000) @@ -2031,7 +2033,6 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2041,16 +2042,16 @@ mod tests { } // An offer with amount_msats(0) must be rejected by the builder per BOLT 12. - match OfferBuilder::new(recipient_pubkey()).amount_msats(0).build() { + match OfferBuilder::new(recipient_pubkey()).amount_msats(0) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(u64::max_value()) @@ -2069,11 +2070,12 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let supported_conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2085,9 +2087,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(2) @@ -2100,10 +2102,11 @@ mod tests { assert_eq!(tlv_stream.amount, None); let invoice_request = OfferBuilder::new(recipient_pubkey()) - .amount(Amount::Currency { - iso4217_code: CurrencyCode::new(*b"USD").unwrap(), - amount: 10, - }) + .amount( + Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }, + &supported_conversion, + ) + .unwrap() .build_unchecked() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() @@ -2124,8 +2127,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) @@ -2137,8 +2140,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) @@ -2163,9 +2166,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::One) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2176,9 +2179,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::One) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(2_000) @@ -2191,9 +2194,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(10_000) @@ -2208,9 +2211,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(11_000) @@ -2223,9 +2226,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(2_000) @@ -2240,9 +2243,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2253,9 +2256,9 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(one)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2275,8 +2278,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .payer_note("bar".into()) @@ -2288,8 +2291,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .payer_note("bar".into()) @@ -2311,8 +2314,8 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) @@ -2335,8 +2338,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2360,8 +2363,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain(Network::Bitcoin) @@ -2378,8 +2381,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .chain_unchecked(Network::Testnet) @@ -2404,11 +2407,12 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let supported_conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2423,7 +2427,6 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(1000) @@ -2440,7 +2443,6 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked_and_sign(); @@ -2458,8 +2460,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats_unchecked(999) @@ -2478,10 +2480,14 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .description("foo".to_string()) - .amount(Amount::Currency { - iso4217_code: CurrencyCode::new(*b"USD").unwrap(), - amount: 1000, - }) + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &supported_conversion, + ) + .unwrap() .build_unchecked() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() @@ -2502,9 +2508,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .quantity(u64::max_value()) @@ -2536,9 +2542,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::One) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2553,9 +2559,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::One) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(2_000) @@ -2578,9 +2584,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(10_000) @@ -2599,9 +2605,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(11_000) @@ -2622,9 +2628,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .amount_msats(2_000) @@ -2643,9 +2649,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked_and_sign(); @@ -2663,9 +2669,9 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Bounded(one)) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked_and_sign(); @@ -2692,8 +2698,8 @@ mod tests { let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked(); @@ -2724,8 +2730,8 @@ mod tests { let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked(); @@ -2754,8 +2760,8 @@ mod tests { let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked(); @@ -2789,8 +2795,8 @@ mod tests { let mut buffer = Vec::new(); OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked() @@ -2817,8 +2823,8 @@ mod tests { let mut invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -2854,8 +2860,8 @@ mod tests { let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let (mut unsigned_invoice_request, payer_keys, _) = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_without_checks(); @@ -2889,8 +2895,8 @@ mod tests { let (mut unsigned_invoice_request, payer_keys, _) = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_without_checks(); @@ -2934,8 +2940,8 @@ mod tests { let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let (mut unsigned_invoice_request, payer_keys, _) = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_without_checks(); @@ -2972,8 +2978,8 @@ mod tests { let (mut unsigned_invoice_request, payer_keys, _) = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_without_checks(); @@ -3007,8 +3013,8 @@ mod tests { let invoice_request = OfferBuilder::new(keys.public_key()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3040,8 +3046,8 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() @@ -3086,9 +3092,9 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .chain(Network::Testnet) .amount_msats(1000) + .unwrap() .supported_quantity(Quantity::Unbounded) - .build() - .unwrap(); + .build(); assert_eq!(offer.issuer_signing_pubkey(), Some(node_id)); // UTF-8 payer note that we can't naively `.truncate(PAYER_NOTE_LIMIT)` diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 1a38fe5441f..4bf3161123d 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -293,6 +293,7 @@ mod tests { use crate::offers::signer::Metadata; use crate::offers::test_utils::recipient_pubkey; use crate::util::ser::Writeable; + use crate::util::test_utils::TestCurrencyConversion; use bitcoin::hashes::{sha256, Hash}; use bitcoin::hex::FromHex; use bitcoin::secp256k1::schnorr::Signature; @@ -335,6 +336,7 @@ mod tests { let nonce = Nonce([0u8; 16]); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let supported_conversion = TestCurrencyConversion; let recipient_pubkey = { let secret_bytes = >::from_hex( @@ -356,10 +358,11 @@ mod tests { // BOLT 12 test vectors let invoice_request = OfferBuilder::new(recipient_pubkey) .description("A Mathematical Treatise".into()) - .amount(Amount::Currency { - iso4217_code: CurrencyCode::new(*b"USD").unwrap(), - amount: 100, - }) + .amount( + Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 100 }, + &supported_conversion, + ) + .unwrap() .build_unchecked() // Override the payer metadata and signing pubkey to match the test vectors .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) @@ -397,8 +400,8 @@ mod tests { let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) - .build() .unwrap() + .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .payer_note("bar".into()) @@ -429,6 +432,7 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey) .amount_msats(100) + .unwrap() .build_unchecked() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() @@ -463,6 +467,7 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey) .amount_msats(100) + .unwrap() .build_unchecked() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index f43084996c6..72752009678 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -45,13 +45,13 @@ //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); //! let offer = OfferBuilder::new(pubkey) //! .description("coffee, large".to_string()) -//! .amount_msats(20_000) +//! .amount_msats(20_000).unwrap() //! .supported_quantity(Quantity::Unbounded) //! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap()) //! .issuer("Foo Bar".to_string()) //! .path(create_blinded_path()) //! .path(create_another_blinded_path()) -//! .build()?; +//! .build(); //! //! // Encode as a bech32 string for use in a QR code. //! let encoded_offer = offer.to_string(); @@ -340,19 +340,32 @@ macro_rules! offer_builder_methods { ( $return_value } - /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`]. + /// Sets the [`Offer::amount`] in millisatoshis. /// - /// Successive calls to this method will override the previous setting. - pub fn amount_msats($self: $self_type, amount_msats: u64) -> $return_type { - $self.amount(Amount::Bitcoin { amount_msats }) + /// Internally this sets the amount as [`Amount::Bitcoin`]. + /// + /// Successive calls to this method override the previously set amount. + pub fn amount_msats($($self_mut)* $self: $self_type, amount_msats: u64) -> Result<$return_type, Bolt12SemanticError> { + if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT { + return Err(Bolt12SemanticError::InvalidAmount); + } + + $self.offer.amount = Some(Amount::Bitcoin { amount_msats }); + Ok($return_value) } /// Sets the [`Offer::amount`]. /// /// Successive calls to this method will override the previous setting. - pub(super) fn amount($($self_mut)* $self: $self_type, amount: Amount) -> $return_type { + pub fn amount($($self_mut)* $self: $self_type, amount: Amount, currency_conversion: &CC) -> Result<$return_type, Bolt12SemanticError> + { + let amount_msats = amount.into_msats(currency_conversion)?; + if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT { + return Err(Bolt12SemanticError::InvalidAmount); + } + $self.offer.amount = Some(amount); - $return_value + Ok($return_value) } /// Sets the [`Offer::absolute_expiry`] as seconds since the Unix epoch. @@ -400,17 +413,7 @@ macro_rules! offer_builder_methods { ( } /// Builds an [`Offer`] from the builder's settings. - pub fn build($($self_mut)* $self: $self_type) -> Result { - match $self.offer.amount { - Some(Amount::Bitcoin { amount_msats }) => { - if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT { - return Err(Bolt12SemanticError::InvalidAmount); - } - }, - Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency), - None => {}, - } - + pub fn build($($self_mut)* $self: $self_type) -> Offer { if $self.offer.amount.is_some() && $self.offer.description.is_none() { $self.offer.description = Some(String::new()); } @@ -421,10 +424,6 @@ macro_rules! offer_builder_methods { ( } } - Ok($self.build_without_checks()) - } - - fn build_without_checks($($self_mut)* $self: $self_type) -> Offer { if let Some(mut metadata) = $self.offer.metadata.take() { // Create the metadata for stateless verification of an InvoiceRequest. if metadata.has_derivation_material() { @@ -512,7 +511,7 @@ macro_rules! offer_builder_test_methods { ( #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn build_unchecked($self: $self_type) -> Offer { - $self.build_without_checks() + $self.build() } } } @@ -709,6 +708,20 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { pub fn issuer_signing_pubkey(&$self) -> Option { $contents.issuer_signing_pubkey() } + + /// Resolves the [`Offer::amount`] into millisatoshis. + /// + /// If the offer amount is denominated in a fiat currency, the provided + /// [`CurrencyConversion`] implementation is used to convert it into msats. + /// + /// Returns: + /// - `Ok(Some(msats))` if the offer specifies an amount and it can be resolved. + /// - `Ok(None)` if the offer does not specify an amount. + /// - `Err(_)` if the amount cannot be resolved (e.g., unsupported currency). + pub fn resolve_offer_amount(&$self, currency_conversion: &CC) -> Result, Bolt12SemanticError> + { + $contents.resolve_offer_amount(currency_conversion) + } } } impl Offer { @@ -994,6 +1007,12 @@ impl OfferContents { self.issuer_signing_pubkey } + pub(super) fn resolve_offer_amount( + &self, currency_conversion: &CC, + ) -> Result, Bolt12SemanticError> { + self.amount().map(|amt| amt.into_msats(currency_conversion)).transpose() + } + pub(super) fn verify_using_metadata( &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1, ) -> Result<(OfferId, Option), ()> { @@ -1426,6 +1445,7 @@ mod tests { use crate::types::features::OfferFeatures; use crate::types::string::PrintableString; use crate::util::ser::{BigSize, Writeable}; + use crate::util::test_utils::TestCurrencyConversion; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1::Secp256k1; @@ -1442,7 +1462,7 @@ mod tests { #[test] fn builds_offer_with_defaults() { - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut buffer = Vec::new(); offer.write(&mut buffer).unwrap(); @@ -1493,30 +1513,24 @@ mod tests { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); - let offer = OfferBuilder::new(pubkey(42)).chain(Network::Bitcoin).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).chain(Network::Bitcoin).build(); assert!(offer.supports_chain(mainnet)); assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().0.chains, None); - let offer = OfferBuilder::new(pubkey(42)).chain(Network::Testnet).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).chain(Network::Testnet).build(); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet])); - let offer = OfferBuilder::new(pubkey(42)) - .chain(Network::Testnet) - .chain(Network::Testnet) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).chain(Network::Testnet).chain(Network::Testnet).build(); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet])); - let offer = OfferBuilder::new(pubkey(42)) - .chain(Network::Bitcoin) - .chain(Network::Testnet) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).chain(Network::Bitcoin).chain(Network::Testnet).build(); assert!(offer.supports_chain(mainnet)); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![mainnet, testnet]); @@ -1525,7 +1539,7 @@ mod tests { #[test] fn builds_offer_with_metadata() { - let offer = OfferBuilder::new(pubkey(42)).metadata(vec![42; 32]).unwrap().build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).metadata(vec![42; 32]).unwrap().build(); assert_eq!(offer.metadata(), Some(&vec![42; 32])); assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![42; 32])); @@ -1534,8 +1548,7 @@ mod tests { .unwrap() .metadata(vec![43; 32]) .unwrap() - .build() - .unwrap(); + .build(); assert_eq!(offer.metadata(), Some(&vec![43; 32])); assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![43; 32])); } @@ -1553,9 +1566,9 @@ mod tests { use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) + .unwrap() .experimental_foo(42) - .build() - .unwrap(); + .build(); assert!(offer.metadata().is_some()); assert_eq!(offer.issuer_signing_pubkey(), Some(node_id)); @@ -1633,10 +1646,10 @@ mod tests { use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) + .unwrap() .path(blinded_path) .experimental_foo(42) - .build() - .unwrap(); + .build(); assert!(offer.metadata().is_none()); assert_ne!(offer.issuer_signing_pubkey(), Some(node_id)); @@ -1700,44 +1713,45 @@ mod tests { let currency_amount = Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }; - let offer = OfferBuilder::new(pubkey(42)).amount_msats(1000).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).amount_msats(1000).unwrap().build(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(offer.amount(), Some(bitcoin_amount)); assert_eq!(tlv_stream.0.amount, Some(1000)); assert_eq!(tlv_stream.0.currency, None); + let conversion = TestCurrencyConversion; + #[cfg(not(c_bindings))] - let builder = OfferBuilder::new(pubkey(42)).amount(currency_amount.clone()); + let builder = OfferBuilder::new(pubkey(42)).amount(currency_amount.clone(), &conversion).unwrap(); #[cfg(c_bindings)] let mut builder = OfferBuilder::new(pubkey(42)); #[cfg(c_bindings)] - builder.amount(currency_amount.clone()); + let _ = builder.amount(currency_amount.clone(), &conversion); + + // Currency-denominated amounts are now supported, so setting the amount should succeed. let tlv_stream = builder.offer.as_tlv_stream(); assert_eq!(builder.offer.amount, Some(currency_amount.clone())); assert_eq!(tlv_stream.0.amount, Some(10)); assert_eq!(tlv_stream.0.currency, Some(b"USD")); - match builder.build() { - Ok(_) => panic!("expected error"), - Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency), - } let offer = OfferBuilder::new(pubkey(42)) - .amount(currency_amount.clone()) - .amount(bitcoin_amount.clone()) - .build() - .unwrap(); + .amount(currency_amount.clone(), &conversion) + .unwrap() + .amount(bitcoin_amount.clone(), &conversion) + .unwrap() + .build(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(tlv_stream.0.amount, Some(1000)); assert_eq!(tlv_stream.0.currency, None); let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; - match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() { + match OfferBuilder::new(pubkey(42)).amount(invalid_amount, &conversion) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } // An amount of 0 must be rejected per BOLT 12. - match OfferBuilder::new(pubkey(42)).amount_msats(0).build() { + match OfferBuilder::new(pubkey(42)).amount_msats(0) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } @@ -1765,37 +1779,33 @@ mod tests { #[test] fn builds_offer_with_description() { - let offer = OfferBuilder::new(pubkey(42)).description("foo".into()).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).description("foo".into()).build(); assert_eq!(offer.description(), Some(PrintableString("foo"))); assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("foo"))); let offer = OfferBuilder::new(pubkey(42)) .description("foo".into()) .description("bar".into()) - .build() - .unwrap(); + .build(); assert_eq!(offer.description(), Some(PrintableString("bar"))); assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("bar"))); - let offer = OfferBuilder::new(pubkey(42)).amount_msats(1000).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).amount_msats(1000).unwrap().build(); assert_eq!(offer.description(), Some(PrintableString(""))); assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from(""))); } #[test] fn builds_offer_with_features() { - let offer = OfferBuilder::new(pubkey(42)) - .features_unchecked(OfferFeatures::unknown()) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).features_unchecked(OfferFeatures::unknown()).build(); assert_eq!(offer.offer_features(), &OfferFeatures::unknown()); assert_eq!(offer.as_tlv_stream().0.features, Some(&OfferFeatures::unknown())); let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .features_unchecked(OfferFeatures::empty()) - .build() - .unwrap(); + .build(); assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.as_tlv_stream().0.features, None); } @@ -1806,7 +1816,7 @@ mod tests { let past_expiry = Duration::from_secs(0); let now = future_expiry - Duration::from_secs(1_000); - let offer = OfferBuilder::new(pubkey(42)).absolute_expiry(future_expiry).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).absolute_expiry(future_expiry).build(); #[cfg(feature = "std")] assert!(!offer.is_expired()); assert!(!offer.is_expired_no_std(now)); @@ -1816,8 +1826,7 @@ mod tests { let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) .absolute_expiry(past_expiry) - .build() - .unwrap(); + .build(); #[cfg(feature = "std")] assert!(offer.is_expired()); assert!(offer.is_expired_no_std(now)); @@ -1846,11 +1855,8 @@ mod tests { ), ]; - let offer = OfferBuilder::new(pubkey(42)) - .path(paths[0].clone()) - .path(paths[1].clone()) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).path(paths[0].clone()).path(paths[1].clone()).build(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(offer.paths(), paths.as_slice()); assert_eq!(offer.issuer_signing_pubkey(), Some(pubkey(42))); @@ -1861,15 +1867,11 @@ mod tests { #[test] fn builds_offer_with_issuer() { - let offer = OfferBuilder::new(pubkey(42)).issuer("foo".into()).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).issuer("foo".into()).build(); assert_eq!(offer.issuer(), Some(PrintableString("foo"))); assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("foo"))); - let offer = OfferBuilder::new(pubkey(42)) - .issuer("foo".into()) - .issuer("bar".into()) - .build() - .unwrap(); + let offer = OfferBuilder::new(pubkey(42)).issuer("foo".into()).issuer("bar".into()).build(); assert_eq!(offer.issuer(), Some(PrintableString("bar"))); assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("bar"))); } @@ -1879,33 +1881,27 @@ mod tests { let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); - let offer = - OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::One).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::One).build(); let tlv_stream = offer.as_tlv_stream(); assert!(!offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::One); assert_eq!(tlv_stream.0.quantity_max, None); - let offer = - OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Unbounded).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Unbounded).build(); let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Unbounded); assert_eq!(tlv_stream.0.quantity_max, Some(0)); - let offer = OfferBuilder::new(pubkey(42)) - .supported_quantity(Quantity::Bounded(ten)) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Bounded(ten)).build(); let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten)); assert_eq!(tlv_stream.0.quantity_max, Some(10)); - let offer = OfferBuilder::new(pubkey(42)) - .supported_quantity(Quantity::Bounded(one)) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Bounded(one)).build(); let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Bounded(one)); @@ -1914,8 +1910,7 @@ mod tests { let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) .supported_quantity(Quantity::One) - .build() - .unwrap(); + .build(); let tlv_stream = offer.as_tlv_stream(); assert!(!offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::One); @@ -1933,7 +1928,6 @@ mod tests { match OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build() - .unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) { Ok(_) => panic!("expected error"), @@ -1943,11 +1937,8 @@ mod tests { #[test] fn parses_offer_with_chains() { - let offer = OfferBuilder::new(pubkey(42)) - .chain(Network::Bitcoin) - .chain(Network::Testnet) - .build() - .unwrap(); + let offer = + OfferBuilder::new(pubkey(42)).chain(Network::Bitcoin).chain(Network::Testnet).build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -1955,10 +1946,11 @@ mod tests { #[test] fn parses_offer_with_amount() { + let conversion = TestCurrencyConversion; let offer = OfferBuilder::new(pubkey(42)) - .amount(Amount::Bitcoin { amount_msats: 1000 }) - .build() - .unwrap(); + .amount(Amount::Bitcoin { amount_msats: 1000 }, &conversion) + .unwrap() + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2090,7 +2082,7 @@ mod tests { #[test] fn parses_offer_with_description() { - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2098,8 +2090,8 @@ mod tests { let offer = OfferBuilder::new(pubkey(42)) .description("foo".to_string()) .amount_msats(1000) - .build() - .unwrap(); + .unwrap() + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2140,8 +2132,7 @@ mod tests { BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, ], )) - .build() - .unwrap(); + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2156,8 +2147,7 @@ mod tests { ], )) .clear_issuer_signing_pubkey() - .build() - .unwrap(); + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2166,7 +2156,7 @@ mod tests { builder.offer.issuer_signing_pubkey = None; builder.offer.paths = Some(vec![]); - let offer = builder.build().unwrap(); + let offer = builder.build(); match offer.to_string().parse::() { Ok(_) => panic!("expected error"), Err(e) => { @@ -2180,30 +2170,26 @@ mod tests { #[test] fn parses_offer_with_quantity() { - let offer = - OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::One).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::One).build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } - let offer = - OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Unbounded).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).supported_quantity(Quantity::Unbounded).build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap())) - .build() - .unwrap(); + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap())) - .build() - .unwrap(); + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2211,7 +2197,7 @@ mod tests { #[test] fn parses_offer_with_issuer_id() { - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2240,7 +2226,7 @@ mod tests { const UNKNOWN_ODD_TYPE: u64 = OFFER_TYPES.end - 1; assert!(UNKNOWN_ODD_TYPE % 2 == 1); - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); @@ -2256,7 +2242,7 @@ mod tests { const UNKNOWN_EVEN_TYPE: u64 = OFFER_TYPES.end - 2; assert!(UNKNOWN_EVEN_TYPE % 2 == 0); - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); @@ -2272,7 +2258,7 @@ mod tests { #[test] fn parses_offer_with_experimental_tlv_records() { - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); @@ -2285,7 +2271,7 @@ mod tests { Err(e) => panic!("error parsing offer: {:?}", e), } - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); @@ -2301,7 +2287,7 @@ mod tests { #[test] fn fails_parsing_offer_with_out_of_range_tlv_records() { - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); @@ -2314,7 +2300,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } - let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index c8afb7cfc12..2f716a25750 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -819,8 +819,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -859,8 +858,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -961,8 +959,7 @@ mod tests { OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .absolute_expiry(future_expiry) - .build() - .unwrap(); + .build(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &valid_offer, @@ -983,8 +980,7 @@ mod tests { OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .absolute_expiry(past_expiry) - .build() - .unwrap(); + .build(); if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( &expired_offer, payment_paths(), @@ -1015,8 +1011,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .experimental_foo(42) - .build() - .unwrap(); + .build(); if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -1061,8 +1056,7 @@ mod tests { let valid_offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); // Error if payment paths are missing. if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( @@ -1128,8 +1122,7 @@ mod tests { let valid_offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let mut offer_missing_issuer_id = valid_offer.clone(); let (mut offer_tlv_stream, _) = offer_missing_issuer_id.as_tlv_stream(); @@ -1165,8 +1158,7 @@ mod tests { .path(blinded_path()) .metadata(vec![42; 32]) .unwrap() - .build() - .unwrap(); + .build(); if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, payment_paths(), @@ -1196,8 +1188,7 @@ mod tests { .path(blinded_path()) .chain(Network::Bitcoin) .chain(Network::Testnet) - .build() - .unwrap(); + .build(); if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer_with_extra_chain, @@ -1226,8 +1217,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); const TEST_RELATIVE_EXPIRY: u32 = 3600; let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( @@ -1268,8 +1258,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -1405,8 +1394,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; assert!(UNKNOWN_ODD_TYPE % 2 == 1); @@ -1499,8 +1487,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -1605,8 +1592,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -1699,8 +1685,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); let offer_id = offer.id(); diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs index d0decdf2c38..23ec5c81b9c 100644 --- a/lightning/src/offers/test_utils.rs +++ b/lightning/src/offers/test_utils.rs @@ -149,8 +149,7 @@ pub fn dummy_static_invoice() -> StaticInvoice { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) - .build() - .unwrap(); + .build(); StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, From 4f3d356213af13a33157938c7826e395157c463c Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 28 Feb 2026 18:39:14 +0530 Subject: [PATCH 5/8] [feat] Keep CurrencyConversion in InvoiceRequestBuilder Retain a `CurrencyConversion` reference on `InvoiceRequestBuilder`. Use that reference for both explicit payer-provided amounts and offer-derived amount lookups, while passing conversion explicitly through `OffersMessageFlow` from `ChannelManager`. Requests that omit `amount_msats` now stay omitted during request construction. The payee resolves those amounts later when building the invoice, so request-time checks only validate explicit payer-supplied amounts. Co-Authored-By: OpenAI Codex --- fuzz/src/offer_deser.rs | 9 +- lightning/src/ln/async_payments_tests.rs | 5 + lightning/src/ln/channelmanager.rs | 31 ++- lightning/src/ln/offers_tests.rs | 28 +- lightning/src/ln/outbound_payment.rs | 14 +- lightning/src/offers/flow.rs | 98 ++++--- lightning/src/offers/invoice.rs | 136 +++++++--- lightning/src/offers/invoice_request.rs | 317 ++++++++++++++--------- lightning/src/offers/merkle.rs | 15 +- lightning/src/offers/offer.rs | 96 ++++--- lightning/src/offers/refund.rs | 7 +- 11 files changed, 498 insertions(+), 258 deletions(-) diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs index 68902ab3150..c17b2764770 100644 --- a/fuzz/src/offer_deser.rs +++ b/fuzz/src/offer_deser.rs @@ -12,9 +12,10 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::TryFrom; use lightning::ln::channelmanager::PaymentId; use lightning::ln::inbound_payment::ExpandedKey; +use lightning::offers::currency::DefaultCurrencyConversion; use lightning::offers::invoice_request::InvoiceRequest; use lightning::offers::nonce::Nonce; -use lightning::offers::offer::{Amount, Offer, Quantity}; +use lightning::offers::offer::{Offer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::sign::EntropySource; use lightning::util::ser::Writeable; @@ -48,13 +49,13 @@ fn build_request(offer: &Offer) -> Result { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = DefaultCurrencyConversion; - let mut builder = offer.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)?; + let mut builder = offer.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion)?; builder = match offer.amount() { None => builder.amount_msats(1000).unwrap(), - Some(Amount::Bitcoin { amount_msats }) => builder.amount_msats(amount_msats + 1)?, - Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency), + Some(amount) => builder.amount_msats(amount.into_msats(&conversion)?)?, }; builder = match offer.supported_quantity() { diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8ca3b747ef4..6abcd63fd85 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -60,6 +60,7 @@ use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::config::{HTLCInterceptionFlags, UserConfig}; use crate::util::ser::Writeable; +use crate::util::test_utils::TestCurrencyConversion; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1; @@ -299,6 +300,7 @@ fn create_static_invoice_builder<'a>( relative_expiry_secs, recipient.node.list_usable_channels(), recipient.node.test_get_peers_for_blinded_path(), + recipient.node.currency_conversion, ) .unwrap() } @@ -1455,6 +1457,8 @@ fn amount_doesnt_match_invreq() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); let release_held_htlc_om_3_0 = pass_async_payments_oms( static_invoice, @@ -1478,6 +1482,7 @@ fn amount_doesnt_match_invreq() { Nonce::from_entropy_source(nodes[0].keys_manager), &secp_ctx, payment_id, + &conversion, ) .unwrap() .amount_msats(amt_msat + 1) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index dc902ae3911..d631c68a05b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5790,6 +5790,7 @@ impl< channels, router, timer_tick_occurred, + &self.currency_conversion, ); match refresh_res { Err(()) => { @@ -5839,6 +5840,7 @@ impl< let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( invoice, + &self.currency_conversion, payment_id, features, best_block_height, @@ -8552,12 +8554,26 @@ impl< }); let verified_invreq = match verify_opt { Some(verified_invreq) => { - if let Some(invreq_amt_msat) = - verified_invreq.amount_msats() + match verified_invreq + .amount_msats(&self.flow.currency_conversion) { - if payment_data.total_msat < invreq_amt_msat { + Ok(invreq_amt_msat) => { + if payment_data.total_msat < invreq_amt_msat { + fail_htlc!(claimable_htlc, payment_hash); + } + }, + Err(_) => { + // `amount_msats()` can only fail if the invoice request does not specify an amount + // and the underlying offer's amount cannot be resolved. + // + // This invoice request corresponds to an offer we constructed, and we only allow + // creating offers with currency amounts that the node explicitly supports. + // + // Therefore, amount resolution must succeed here. Reaching this branch indicates + // an internal logic error. + debug_assert!(false); fail_htlc!(claimable_htlc, payment_hash); - } + }, } verified_invreq }, @@ -14709,17 +14725,19 @@ impl< let nonce = Nonce::from_entropy_source(entropy); let builder = self.flow.create_invoice_request_builder( - offer, nonce, payment_id, + offer, nonce, payment_id, &self.currency_conversion, )?; let builder = match quantity { None => builder, Some(quantity) => builder.quantity(quantity)?, }; + let builder = match amount_msats { None => builder, Some(amount_msats) => builder.amount_msats(amount_msats)?, }; + let builder = match payer_note { None => builder, Some(payer_note) => builder.payer_note(payer_note), @@ -16976,6 +16994,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + &self.currency_conversion, ); match result { @@ -17000,6 +17019,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + &self.currency_conversion, ); match result { @@ -17141,6 +17161,7 @@ impl< self.list_usable_channels(), &self.entropy_source, &self.router, + &self.currency_conversion, ) { Some((msg, ctx)) => (msg, ctx), None => return None, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6b19f4b783a..d4bc3bc9edb 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -73,6 +73,7 @@ use crate::util::ser::Writeable; const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); use crate::prelude::*; +use crate::util::test_utils::TestCurrencyConversion; macro_rules! expect_recent_payment { ($node: expr, $payment_state: path, $payment_id: expr) => {{ @@ -517,12 +518,14 @@ fn check_dummy_hop_pattern_in_offer() { } let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + bob.node.pay_for_offer(&compact_offer, None, payment_id, Default::default()).unwrap(); let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH)); @@ -544,7 +547,7 @@ fn check_dummy_hop_pattern_in_offer() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH)); } @@ -706,6 +709,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { } let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -729,7 +734,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert!(check_dummy_hopped_path_length(&reply_path, bob, charlie_id, DUMMY_HOPS_PATH_LENGTH)); @@ -871,6 +876,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { } let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -887,7 +893,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH)); @@ -1253,6 +1259,7 @@ fn creates_and_pays_for_offer_with_retry() { assert!(check_compact_path_introduction_node(&path, bob, alice_id)); } let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1276,7 +1283,7 @@ fn creates_and_pays_for_offer_with_retry() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1576,6 +1583,8 @@ fn fails_authentication_when_handling_invoice_request() { // Send the invoice request directly to Alice instead of using a blinded path. let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1590,7 +1599,7 @@ fn fails_authentication_when_handling_invoice_request() { alice.onion_messenger.handle_onion_message(david_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH)); @@ -1619,7 +1628,7 @@ fn fails_authentication_when_handling_invoice_request() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH)); @@ -1693,6 +1702,8 @@ fn fails_authentication_when_handling_invoice_for_offer() { }; let payment_id = PaymentId([2; 32]); + let conversion = TestCurrencyConversion; + david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1719,7 +1730,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert!(check_dummy_hopped_path_length(&reply_path, david, charlie_id, DUMMY_HOPS_PATH_LENGTH)); @@ -1973,6 +1984,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { .create_offer_builder().unwrap() .clear_chains() .chain(Network::Signet) + .amount_msats(1_000).unwrap() .build(); match bob.node.pay_for_offer(&offer, None, PaymentId([1; 32]), Default::default()) { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index bf9132c665e..2766cc9c355 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2882,7 +2882,7 @@ mod tests { use crate::util::errors::APIError; use crate::util::hash_tables::new_hash_map; use crate::util::logger::WithContext; - use crate::util::test_utils; + use crate::util::test_utils::{self, TestCurrencyConversion}; use alloc::collections::VecDeque; @@ -3245,6 +3245,7 @@ mod tests { let pending_events = Mutex::new(VecDeque::new()); let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); + let conversion = TestCurrencyConversion; let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); assert!( @@ -3258,7 +3259,7 @@ mod tests { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000).unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap() .build().unwrap() @@ -3302,12 +3303,13 @@ mod tests { let expanded_key = ExpandedKey::new([42; 32]); let nonce = Nonce([0; 16]); let payment_id = PaymentId([0; 32]); + let conversion = TestCurrencyConversion; let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000).unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() @@ -3367,12 +3369,13 @@ mod tests { let expanded_key = ExpandedKey::new([42; 32]); let nonce = Nonce([0; 16]); let payment_id = PaymentId([0; 32]); + let conversion = TestCurrencyConversion; let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000).unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() @@ -3457,11 +3460,12 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; OfferBuilder::new(recipient_pubkey()) .amount_msats(1000).unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 36881427dc5..b617b431ed3 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -822,19 +822,25 @@ impl OffersMessageFlow { /// by the [`OffersMessageFlow`], and any corresponding [`Bolt12Invoice`] received in response /// can be verified using [`Self::verify_bolt12_invoice`]. /// + /// The provided [`CurrencyConversion`] is used to resolve any currency-denominated offer amount + /// while building the request. + /// /// # Nonce /// The nonce is used to create a unique [`InvoiceRequest::payer_metadata`] for the invoice request. /// These will be used to verify the corresponding [`Bolt12Invoice`] when it is received. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. - pub fn create_invoice_request_builder<'a>( + pub fn create_invoice_request_builder<'a, CC: CurrencyConversion>( &'a self, offer: &'a Offer, nonce: Nonce, payment_id: PaymentId, - ) -> Result, Bolt12SemanticError> { + currency_conversion: &'a CC, + ) -> Result, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; - let builder: InvoiceRequestBuilder = - offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id)?.into(); + let builder: InvoiceRequestBuilder<'a, 'a, secp256k1::All, CC> = offer + .request_invoice(expanded_key, nonce, secp_ctx, payment_id, currency_conversion)? + .into(); + let builder = builder.chain_hash(self.chain_hash)?; Ok(builder) @@ -843,11 +849,14 @@ impl OffersMessageFlow { /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were /// created via [`Self::create_async_receive_offer_builder`]. /// + /// The provided [`CurrencyConversion`] is used to resolve the offer amount when the offer is + /// currency-denominated. + /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. - pub fn create_static_invoice_builder<'a, R: Router>( + pub fn create_static_invoice_builder<'a, R: Router, CC: CurrencyConversion>( &self, router: &R, offer: &'a Offer, offer_nonce: Nonce, payment_secret: PaymentSecret, relative_expiry_secs: u32, usable_channels: Vec, - peers: Vec, + peers: Vec, currency_conversion: &CC, ) -> Result, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; @@ -855,10 +864,7 @@ impl OffersMessageFlow { let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); - let amount_msat = offer.amount().and_then(|amount| match amount { - Amount::Bitcoin { amount_msats } => Some(amount_msats), - Amount::Currency { .. } => None, - }); + let amount_msat = offer.resolve_offer_amount(currency_conversion)?; let created_at = self.duration_since_epoch(); @@ -973,23 +979,32 @@ impl OffersMessageFlow { /// /// Use this method when you want to inspect or modify the [`InvoiceBuilder`] /// before signing and generating the final [`Bolt12Invoice`]. + /// The provided [`CurrencyConversion`] is used to resolve any + /// currency-denominated amount in the [`InvoiceRequest`]. /// /// # 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_with_keys<'a, R: Router, F>( + pub fn create_invoice_builder_from_invoice_request_with_keys< + 'a, + R: Router, + F, + CC: CurrencyConversion, + >( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest, - usable_channels: Vec, get_payment_info: F, + usable_channels: Vec, get_payment_info: F, currency_conversion: &CC, ) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let amount_msats = - InvoiceBuilder::::amount_msats(&invoice_request.inner)?; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + currency_conversion, + )?; let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; @@ -1009,10 +1024,15 @@ impl OffersMessageFlow { ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - #[cfg(all(feature = "std", not(fuzzing)))] - let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash); - #[cfg(any(not(feature = "std"), fuzzing))] + #[cfg(feature = "std")] + let builder = invoice_request.respond_using_derived_keys( + currency_conversion, + payment_paths, + payment_hash, + ); + #[cfg(not(feature = "std"))] let builder = invoice_request.respond_using_derived_keys_no_std( + currency_conversion, payment_paths, payment_hash, Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64), @@ -1032,23 +1052,32 @@ impl OffersMessageFlow { /// /// Use this method when you want to inspect or modify the [`InvoiceBuilder`] /// before signing and generating the final [`Bolt12Invoice`]. + /// The provided [`CurrencyConversion`] is used to resolve any + /// currency-denominated amount in the [`InvoiceRequest`]. /// /// # 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_without_keys<'a, R: Router, F>( + pub fn create_invoice_builder_from_invoice_request_without_keys< + 'a, + R: Router, + F, + CC: CurrencyConversion, + >( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest, - usable_channels: Vec, get_payment_info: F, + usable_channels: Vec, get_payment_info: F, currency_conversion: &CC, ) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let amount_msats = - InvoiceBuilder::::amount_msats(&invoice_request.inner)?; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + currency_conversion, + )?; let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; @@ -1068,10 +1097,11 @@ impl OffersMessageFlow { ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - #[cfg(all(feature = "std", not(fuzzing)))] - let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(any(not(feature = "std"), fuzzing))] + #[cfg(feature = "std")] + let builder = invoice_request.respond_with(currency_conversion, payment_paths, payment_hash); + #[cfg(not(feature = "std"))] let builder = invoice_request.respond_with_no_std( + currency_conversion, payment_paths, payment_hash, Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64), @@ -1389,9 +1419,9 @@ impl OffersMessageFlow { /// the cache can self-regulate the number of messages sent out. /// /// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message. - pub fn check_refresh_async_receive_offer_cache( + pub fn check_refresh_async_receive_offer_cache( &self, peers: Vec, usable_channels: Vec, router: R, - timer_tick_occurred: bool, + timer_tick_occurred: bool, currency_conversion: &CC, ) -> Result<(), ()> { // Terminate early if this node does not intend to receive async payments. { @@ -1404,7 +1434,7 @@ impl OffersMessageFlow { self.check_refresh_async_offers(peers.clone(), timer_tick_occurred)?; if timer_tick_occurred { - self.check_refresh_static_invoices(peers, usable_channels, router); + self.check_refresh_static_invoices(peers, usable_channels, router, currency_conversion); } Ok(()) @@ -1461,8 +1491,9 @@ impl OffersMessageFlow { /// Enqueue onion messages that will used to request invoice refresh from the static invoice /// server, based on the offers provided by the cache. - fn check_refresh_static_invoices( + fn check_refresh_static_invoices( &self, peers: Vec, usable_channels: Vec, router: R, + currency_conversion: &CC, ) { let mut serve_static_invoice_msgs = Vec::new(); { @@ -1477,6 +1508,7 @@ impl OffersMessageFlow { peers.clone(), usable_channels.clone(), &router, + currency_conversion, ) { Ok((invoice, path)) => (invoice, path), Err(()) => continue, @@ -1584,10 +1616,10 @@ impl OffersMessageFlow { /// /// Returns `None` if we have enough offers cached already, verification of `message` fails, or we /// fail to create blinded paths. - pub fn handle_offer_paths( + pub fn handle_offer_paths( &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, peers: Vec, usable_channels: Vec, entropy: ES, - router: R, + router: R, currency_conversion: &CC, ) -> Option<(ServeStaticInvoice, MessageContext)> { let duration_since_epoch = self.duration_since_epoch(); let invoice_slot = match context { @@ -1632,6 +1664,7 @@ impl OffersMessageFlow { peers, usable_channels, router, + currency_conversion, ) { Ok(res) => res, Err(()) => { @@ -1665,9 +1698,9 @@ impl OffersMessageFlow { /// Creates a [`StaticInvoice`] and a blinded path for the server to forward invoice requests from /// payers to our node. - fn create_static_invoice_for_server( + fn create_static_invoice_for_server( &self, offer: &Offer, offer_nonce: Nonce, peers: Vec, - usable_channels: Vec, router: R, + usable_channels: Vec, router: R, currency_conversion: &CC, ) -> Result<(StaticInvoice, BlindedMessagePath), ()> { let expanded_key = &self.inbound_payment_key; let duration_since_epoch = self.duration_since_epoch(); @@ -1699,6 +1732,7 @@ impl OffersMessageFlow { offer_relative_expiry, usable_channels, peers.clone(), + currency_conversion, ) .and_then(|builder| builder.build_and_sign(secp_ctx)) .map_err(|_| ())?; diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 6e6e168e01a..9f9f57501b8 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -121,6 +121,7 @@ use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; +use crate::offers::currency::DefaultCurrencyConversion; #[cfg(test)] use crate::offers::invoice_macros::invoice_builder_methods_test_common; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; @@ -1769,10 +1770,35 @@ impl TryFrom for InvoiceContents { experimental_invoice_request_tlv_stream, ))?; - if let Some(requested_amount_msats) = invoice_request.amount_msats() { - if amount_msats != requested_amount_msats { - return Err(Bolt12SemanticError::InvalidAmount); - } + // Note: + // It is safe to use `DefaultCurrencyConversion` here. + // + // `amount_msats()` can fail only if: + // 1. The computed payable amount exceeds the maximum lightning-payable amount, or + // 2. The invoice request has no explicit amount while the underlying offer + // is currency-denominated. + // + // Neither can occur here: + // - Invalid payable amounts are rejected earlier. + // - For currency-denominated offers we always set an explicit amount + // when constructing the invoice request. + // + // Since this invoice corresponds to the invoice request we created, + // `amount_msats()` must succeed here. + let requested_amount_msats = + match invoice_request.amount_msats(&DefaultCurrencyConversion) { + Ok(msats) => msats, + Err(_) => { + debug_assert!( + false, + "amount_msats must succeed for invoice requests constructed by LDK" + ); + return Err(Bolt12SemanticError::InvalidAmount); + }, + }; + + if amount_msats != requested_amount_msats { + return Err(Bolt12SemanticError::InvalidAmount); } Ok(InvoiceContents::ForOffer { invoice_request, fields }) @@ -1860,6 +1886,7 @@ mod tests { use crate::types::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::types::string::PrintableString; use crate::util::ser::{BigSize, Iterable, Writeable}; + use crate::util::test_utils::TestCurrencyConversion; #[cfg(not(c_bindings))] use {crate::offers::offer::OfferBuilder, crate::offers::refund::RefundBuilder}; #[cfg(c_bindings)] @@ -1891,6 +1918,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); let payment_paths = payment_paths(); @@ -1900,7 +1928,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2162,6 +2190,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); @@ -2171,7 +2200,7 @@ mod tests { .unwrap() .absolute_expiry(future_expiry) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2187,7 +2216,7 @@ mod tests { .unwrap() .absolute_expiry(past_expiry) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign() .respond_with(payment_paths(), payment_hash()) @@ -2239,6 +2268,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let blinded_path = BlindedMessagePath::from_blinded_path( pubkey(40), @@ -2258,7 +2288,7 @@ mod tests { .path(blinded_path) .experimental_foo(42) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2361,6 +2391,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let now = now(); let one_hour = Duration::from_secs(3600); @@ -2369,7 +2400,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2390,7 +2421,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2415,12 +2446,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1001) .unwrap() @@ -2444,13 +2476,14 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(2) .unwrap() @@ -2471,7 +2504,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(u64::max_value()) .unwrap() @@ -2490,6 +2523,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let script = ScriptBuf::new(); let pubkey = bitcoin::key::PublicKey::new(recipient_pubkey()); @@ -2500,7 +2534,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2548,6 +2582,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); @@ -2556,7 +2591,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2579,12 +2614,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2602,7 +2638,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2624,12 +2660,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2701,12 +2738,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2745,12 +2783,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2778,12 +2817,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2822,12 +2862,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2864,12 +2905,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -2901,6 +2943,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let script = ScriptBuf::new(); let pubkey = bitcoin::key::PublicKey::new(recipient_pubkey()); @@ -2911,7 +2954,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2970,12 +3013,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3028,6 +3072,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let paths = [ BlindedMessagePath::from_blinded_path( @@ -3062,7 +3107,7 @@ mod tests { .path(paths[0].clone()) .path(paths[1].clone()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3092,7 +3137,7 @@ mod tests { .path(paths[0].clone()) .path(paths[1].clone()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3129,12 +3174,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3161,7 +3207,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1000) .unwrap() @@ -3221,13 +3267,14 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mut buffer = Vec::new(); OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3255,12 +3302,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mut invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3293,6 +3341,7 @@ mod tests { let entropy = FixedEntropy {}; let nonce = Nonce::from_entropy_source(&entropy); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; assert!(UNKNOWN_ODD_TYPE % 2 == 1); @@ -3303,7 +3352,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3342,7 +3391,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3381,6 +3430,7 @@ mod tests { let entropy = FixedEntropy {}; let nonce = Nonce::from_entropy_source(&entropy); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let secp_ctx = Secp256k1::new(); let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); @@ -3388,7 +3438,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3414,7 +3464,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3455,7 +3505,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3493,7 +3543,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3529,12 +3579,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3564,12 +3615,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap() @@ -3609,13 +3661,14 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let offer = OfferBuilder::new(recipient_pubkey()).amount_msats(1000).unwrap().build(); let offer_id = offer.id(); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -3657,11 +3710,12 @@ mod tests { let payment_paths = payment_paths(); let now = Duration::from_secs(123456); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let offer = OfferBuilder::new(node_id).amount_msats(1000).unwrap().build(); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 1ee1b71cff3..3ebfed85a65 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -28,6 +28,7 @@ //! use lightning::ln::channelmanager::PaymentId; //! use lightning::ln::inbound_payment::ExpandedKey; //! use lightning::types::features::OfferFeatures; +//! use lightning::offers::currency::DefaultCurrencyConversion; //! use lightning::offers::invoice_request::UnsignedInvoiceRequest; //! # use lightning::offers::nonce::Nonce; //! use lightning::offers::offer::Offer; @@ -46,13 +47,14 @@ //! # let nonce = Nonce::from_entropy_source(&entropy); //! let secp_ctx = Secp256k1::new(); //! let payment_id = PaymentId([1; 32]); +//! let conversion = DefaultCurrencyConversion; //! let mut buffer = Vec::new(); //! //! # use lightning::offers::invoice_request::InvoiceRequestBuilder; -//! # >::from( +//! # >::from( //! "lno1qcp4256ypq" //! .parse::()? -//! .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)? +//! .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion)? //! # ) //! .chain(Network::Testnet)? //! .amount_msats(1000)? @@ -70,16 +72,16 @@ use crate::blinded_path::payment::BlindedPaymentPath; use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; -use crate::ln::msgs::DecodeError; -use crate::offers::currency::CurrencyConversion; +use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::currency::{CurrencyConversion, DefaultCurrencyConversion}; use crate::offers::invoice::{DerivedSigningPubkey, ExplicitSigningPubkey, SigningPubkeyStrategy}; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, - OfferId, OfferTlvStream, OfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, + ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, OfferId, + OfferTlvStream, OfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -119,11 +121,12 @@ pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; /// This is not exported to bindings users as builder patterns don't map outside of move semantics. /// /// [module-level documentation]: self -pub struct InvoiceRequestBuilder<'a, 'b, T: secp256k1::Signing> { +pub struct InvoiceRequestBuilder<'a, 'b, T: secp256k1::Signing, CC: CurrencyConversion> { offer: &'a Offer, invoice_request: InvoiceRequestContentsWithoutPayerSigningPubkey, payer_signing_pubkey: Option, secp_ctx: Option<&'b Secp256k1>, + currency_conversion: &'a CC, } /// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow. @@ -132,11 +135,12 @@ pub struct InvoiceRequestBuilder<'a, 'b, T: secp256k1::Signing> { /// /// [module-level documentation]: self #[cfg(c_bindings)] -pub struct InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b> { +pub struct InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC: CurrencyConversion> { offer: &'a Offer, invoice_request: InvoiceRequestContentsWithoutPayerSigningPubkey, payer_signing_pubkey: Option, secp_ctx: Option<&'b Secp256k1>, + currency_conversion: &'a CC, } macro_rules! invoice_request_derived_payer_signing_pubkey_builder_methods { @@ -147,6 +151,7 @@ macro_rules! invoice_request_derived_payer_signing_pubkey_builder_methods { pub(super) fn deriving_signing_pubkey( offer: &'a Offer, expanded_key: &ExpandedKey, nonce: Nonce, secp_ctx: &'b Secp256k1<$secp_context>, payment_id: PaymentId, + currency_conversion: &'a CC, ) -> Self { let payment_id = Some(payment_id); let derivation_material = MetadataMaterial::new(nonce, expanded_key, payment_id); @@ -156,6 +161,7 @@ macro_rules! invoice_request_derived_payer_signing_pubkey_builder_methods { invoice_request: Self::create_contents(offer, metadata), payer_signing_pubkey: None, secp_ctx: Some(secp_ctx), + currency_conversion, } } @@ -224,7 +230,7 @@ macro_rules! invoice_request_builder_methods { ( /// [`quantity`]: Self::quantity pub fn amount_msats($($self_mut)* $self: $self_type, amount_msats: u64) -> Result<$return_type, Bolt12SemanticError> { $self.invoice_request.offer.check_amount_msats_for_quantity( - Some(amount_msats), $self.invoice_request.quantity + $self.currency_conversion, Some(amount_msats), $self.invoice_request.quantity )?; $self.invoice_request.amount_msats = Some(amount_msats); Ok($return_value) @@ -280,9 +286,13 @@ macro_rules! invoice_request_builder_methods { ( } $self.invoice_request.offer.check_quantity($self.invoice_request.quantity)?; - $self.invoice_request.offer.check_amount_msats_for_quantity( - $self.invoice_request.amount_msats, $self.invoice_request.quantity - )?; + if let Some(amount_msats) = $self.invoice_request.amount_msats { + // Omitted amounts are resolved when the payee builds the invoice, so only + // explicit payer-provided amounts need request-time validation here. + $self.invoice_request.offer.check_amount_msats_for_quantity( + $self.currency_conversion, Some(amount_msats), $self.invoice_request.quantity + )?; + } Ok($self.build_without_checks()) } @@ -401,7 +411,7 @@ macro_rules! invoice_request_builder_test_methods { ( } } } -impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, T> { +impl<'a, 'b, T: secp256k1::Signing, CC: CurrencyConversion> InvoiceRequestBuilder<'a, 'b, T, CC> { invoice_request_derived_payer_signing_pubkey_builder_methods!(self, Self, T); invoice_request_builder_methods!(self, Self, Self, self, T, mut); @@ -410,31 +420,37 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, T> { } #[cfg(all(c_bindings, not(test)))] -impl<'a, 'b> InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b> { +impl<'a, 'b, CC: CurrencyConversion> + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC> +{ invoice_request_derived_payer_signing_pubkey_builder_methods!(self, &mut Self, secp256k1::All); invoice_request_builder_methods!(self, &mut Self, (), (), secp256k1::All); } #[cfg(all(c_bindings, test))] -impl<'a, 'b> InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b> { +impl<'a, 'b, CC: CurrencyConversion> + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC> +{ invoice_request_derived_payer_signing_pubkey_builder_methods!(self, &mut Self, secp256k1::All); invoice_request_builder_methods!(self, &mut Self, &mut Self, self, secp256k1::All); invoice_request_builder_test_methods!(self, &mut Self, &mut Self, self); } #[cfg(c_bindings)] -impl<'a, 'b> From> - for InvoiceRequestBuilder<'a, 'b, secp256k1::All> +impl<'a, 'b, CC: CurrencyConversion> + From> + for InvoiceRequestBuilder<'a, 'b, secp256k1::All, CC> { - fn from(builder: InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>) -> Self { + fn from(builder: InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC>) -> Self { let InvoiceRequestWithDerivedPayerSigningPubkeyBuilder { offer, invoice_request, payer_signing_pubkey, secp_ctx, + currency_conversion, } = builder; - Self { offer, invoice_request, payer_signing_pubkey, secp_ctx } + Self { offer, invoice_request, payer_signing_pubkey, secp_ctx, currency_conversion } } } @@ -705,12 +721,21 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { $contents.chain() } - /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which - /// must be greater than or equal to [`Offer::amount`], converted if necessary. + /// Returns the total amount requested by this invoice request, in millisatoshis. /// - /// [`chain`]: Self::chain - pub fn amount_msats(&$self) -> Option { - $contents.amount_msats() + /// If the invoice request explicitly sets an amount, that value is returned. + /// Otherwise, the amount is derived from [`Offer::amount`], multiplied by the + /// requested [`quantity`], and converted to millisatoshis if the offer amount + /// is currency-denominated. + /// + /// This returns an error if the effective amount is semantically invalid + /// (for example due to unsupported currency conversion or arithmetic overflow). + /// + /// [`amount_msats`]: Self::amount_msats + /// [`quantity`]: Self::quantity + pub fn amount_msats(&$self, currency_conversion: &CC) -> Result + { + $contents.amount_msats(currency_conversion) } /// Returns whether an amount was set in the request; otherwise, if [`amount_msats`] is `Some` @@ -949,7 +974,7 @@ impl InvoiceRequest { invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self, - InvoiceWithExplicitSigningPubkeyBuilder + InvoiceWithExplicitSigningPubkeyBuilder<'_> ); invoice_request_verify_method!(self, &Self); @@ -1084,7 +1109,7 @@ impl VerifiedInvoiceRequest { invoice_request_respond_with_derived_signing_pubkey_methods!( self, self.inner, - InvoiceWithDerivedSigningPubkeyBuilder + InvoiceWithDerivedSigningPubkeyBuilder<'_> ); } @@ -1103,7 +1128,7 @@ impl VerifiedInvoiceRequest { invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self.inner, - InvoiceWithExplicitSigningPubkeyBuilder + InvoiceWithExplicitSigningPubkeyBuilder<'_> ); } @@ -1143,17 +1168,26 @@ impl InvoiceRequestContents { self.inner.chain() } - pub(super) fn amount_msats(&self) -> Option { - self.inner.amount_msats().or_else(|| match self.inner.offer.amount() { - Some(Amount::Bitcoin { amount_msats }) => { - Some(amount_msats.saturating_mul(self.quantity().unwrap_or(1))) - }, - Some(Amount::Currency { .. }) => None, + pub(super) fn amount_msats( + &self, currency_conversion: &CC, + ) -> Result { + match self.inner.amount_msats() { + Some(msats) => Ok(msats), None => { - debug_assert!(false); - None + let unit_msats = self + .inner + .offer + .resolve_offer_amount(currency_conversion)? + .ok_or(Bolt12SemanticError::MissingAmount)?; + + let quantity = self.quantity().unwrap_or(1); + + unit_msats + .checked_mul(quantity) + .filter(|msats| *msats <= MAX_VALUE_MSAT) + .ok_or(Bolt12SemanticError::InvalidAmount) }, - }) + } } pub(super) fn has_amount_msats(&self) -> bool { @@ -1458,7 +1492,15 @@ impl TryFrom for InvoiceRequestContents { } offer.check_quantity(quantity)?; - offer.check_amount_msats_for_quantity(amount, quantity)?; + + match offer.check_amount_msats_for_quantity(&DefaultCurrencyConversion, amount, quantity) { + // If the offer amount is currency-denominated, we intentionally skip the + // amount check here, as currency conversion is not available at this stage. + // The corresponding validation is performed when handling the Invoice Request, + // i.e., during InvoiceBuilder creation. + Ok(()) | Err(Bolt12SemanticError::UnsupportedCurrency) => (), + Err(err) => return Err(err), + } let features = features.unwrap_or_else(InvoiceRequestFeatures::empty); @@ -1557,6 +1599,7 @@ mod tests { use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::currency::DefaultCurrencyConversion; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; use crate::offers::invoice_request::string_truncate_safe; use crate::offers::merkle::{self, SignatureTlvStreamRef, TaggedHash, TlvStream}; @@ -1589,13 +1632,14 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1619,7 +1663,7 @@ mod tests { assert_eq!(invoice_request.supported_quantity(), Quantity::One); assert_eq!(invoice_request.issuer_signing_pubkey(), Some(recipient_pubkey())); assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); - assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(1000)); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(invoice_request.quantity(), None); assert_eq!(invoice_request.payer_note(), None); @@ -1679,6 +1723,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); @@ -1688,7 +1733,7 @@ mod tests { .unwrap() .absolute_expiry(future_expiry) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -1700,7 +1745,7 @@ mod tests { .unwrap() .absolute_expiry(past_expiry) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -1716,6 +1761,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) @@ -1723,7 +1769,7 @@ mod tests { .experimental_foo(42) .build(); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .experimental_bar(42) .build_and_sign() @@ -1826,6 +1872,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); @@ -1834,7 +1881,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Bitcoin) .unwrap() @@ -1849,7 +1896,7 @@ mod tests { .unwrap() .chain(Network::Testnet) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Testnet) .unwrap() @@ -1865,7 +1912,7 @@ mod tests { .chain(Network::Bitcoin) .chain(Network::Testnet) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Bitcoin) .unwrap() @@ -1881,7 +1928,7 @@ mod tests { .chain(Network::Bitcoin) .chain(Network::Testnet) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Bitcoin) .unwrap() @@ -1898,7 +1945,7 @@ mod tests { .unwrap() .chain(Network::Testnet) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Bitcoin) { @@ -1911,7 +1958,7 @@ mod tests { .unwrap() .chain(Network::Testnet) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -1927,12 +1974,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1000) .unwrap() @@ -1940,14 +1988,14 @@ mod tests { .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(1000)); assert_eq!(tlv_stream.amount, Some(1000)); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1001) .unwrap() @@ -1957,14 +2005,14 @@ mod tests { .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(1000)); assert_eq!(tlv_stream.amount, Some(1000)); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1001) .unwrap() @@ -1972,14 +2020,14 @@ mod tests { .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), Some(1001)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(1001)); assert_eq!(tlv_stream.amount, Some(1001)); match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(999) { @@ -1992,7 +2040,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(2) .unwrap() @@ -2006,7 +2054,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(MAX_VALUE_MSAT + 1) { @@ -2019,7 +2067,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1000) .unwrap() @@ -2033,7 +2081,7 @@ mod tests { match OfferBuilder::new(recipient_pubkey()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -2052,7 +2100,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(u64::max_value()) .unwrap() @@ -2070,19 +2118,20 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let supported_conversion = TestCurrencyConversion; + let conversion = TestCurrencyConversion; + let unsupported_conversion = DefaultCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(!invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(1000)); assert_eq!(tlv_stream.amount, None); let invoice_request = OfferBuilder::new(recipient_pubkey()) @@ -2090,7 +2139,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(2) .unwrap() @@ -2098,22 +2147,39 @@ mod tests { .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(!invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), Some(2000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(2000)); assert_eq!(tlv_stream.amount, None); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount( Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }, - &supported_conversion, + &conversion, ) .unwrap() .build_unchecked() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .build_unchecked_and_sign(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(!invoice_request.has_amount_msats()); + assert!(matches!( + invoice_request.amount_msats(&unsupported_conversion), + Err(Bolt12SemanticError::UnsupportedCurrency) + )); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000)); + assert_eq!(tlv_stream.amount, None); + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .build() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert!(!invoice_request.has_amount_msats()); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!( + invoice_request.amount_msats(&conversion), + Err(Bolt12SemanticError::MissingAmount) + ); assert_eq!(tlv_stream.amount, None); } @@ -2124,12 +2190,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) .build_and_sign() @@ -2142,7 +2209,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) .features_unchecked(InvoiceRequestFeatures::empty()) @@ -2160,6 +2227,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); @@ -2169,7 +2237,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::One) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2182,7 +2250,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::One) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(2_000) .unwrap() @@ -2197,7 +2265,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(10_000) .unwrap() @@ -2206,7 +2274,7 @@ mod tests { .build_and_sign() .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); - assert_eq!(invoice_request.amount_msats(), Some(10_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(10_000)); assert_eq!(tlv_stream.amount, Some(10_000)); match OfferBuilder::new(recipient_pubkey()) @@ -2214,7 +2282,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(11_000) .unwrap() @@ -2229,7 +2297,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(2_000) .unwrap() @@ -2238,7 +2306,7 @@ mod tests { .build_and_sign() .unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); - assert_eq!(invoice_request.amount_msats(), Some(2_000)); + assert_eq!(invoice_request.amount_msats(&conversion), Ok(2_000)); assert_eq!(tlv_stream.amount, Some(2_000)); match OfferBuilder::new(recipient_pubkey()) @@ -2246,7 +2314,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -2259,7 +2327,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(one)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() { @@ -2275,12 +2343,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .payer_note("bar".into()) .build_and_sign() @@ -2293,7 +2362,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .payer_note("bar".into()) .payer_note("baz".into()) @@ -2311,12 +2380,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) .build_and_sign() @@ -2335,12 +2405,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2360,12 +2431,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Bitcoin) .unwrap() @@ -2383,7 +2455,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain_unchecked(Network::Testnet) .build_unchecked_and_sign(); @@ -2407,13 +2479,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let supported_conversion = TestCurrencyConversion; + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2427,7 +2499,7 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(1000) .unwrap() @@ -2443,7 +2515,7 @@ mod tests { let invoice_request = OfferBuilder::new(recipient_pubkey()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign(); @@ -2462,7 +2534,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats_unchecked(999) .build_unchecked_and_sign(); @@ -2485,25 +2557,20 @@ mod tests { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 1000, }, - &supported_conversion, + &conversion, ) .unwrap() .build_unchecked() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign(); let mut buffer = Vec::new(); invoice_request.write(&mut buffer).unwrap(); - match InvoiceRequest::try_from(buffer) { - Ok(_) => panic!("expected error"), - Err(e) => { - assert_eq!( - e, - Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::UnsupportedCurrency) - ); - }, + // Parsing must succeed now that LDK supports Offers with currency-denominated amounts. + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); } let invoice_request = OfferBuilder::new(recipient_pubkey()) @@ -2511,7 +2578,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .quantity(u64::max_value()) .unwrap() @@ -2536,6 +2603,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); @@ -2545,7 +2613,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::One) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2562,7 +2630,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::One) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(2_000) .unwrap() @@ -2587,7 +2655,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(10_000) .unwrap() @@ -2608,7 +2676,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(ten)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(11_000) .unwrap() @@ -2631,7 +2699,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .amount_msats(2_000) .unwrap() @@ -2652,7 +2720,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Unbounded) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign(); @@ -2672,7 +2740,7 @@ mod tests { .unwrap() .supported_quantity(Quantity::Bounded(one)) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign(); @@ -2695,12 +2763,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked(); let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); @@ -2727,12 +2796,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked(); let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); @@ -2757,12 +2827,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked(); let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); @@ -2791,13 +2862,14 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mut buffer = Vec::new(); OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked() .contents @@ -2820,12 +2892,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let mut invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -2852,6 +2925,7 @@ mod tests { let entropy = FixedEntropy {}; let nonce = Nonce::from_entropy_source(&entropy); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1; assert!(UNKNOWN_ODD_TYPE % 2 == 1); @@ -2862,7 +2936,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_without_checks(); @@ -2897,7 +2971,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_without_checks(); @@ -2932,6 +3006,7 @@ mod tests { let entropy = FixedEntropy {}; let nonce = Nonce::from_entropy_source(&entropy); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start + 1; assert!(UNKNOWN_ODD_TYPE % 2 == 1); @@ -2942,7 +3017,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_without_checks(); @@ -2980,7 +3055,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_without_checks(); @@ -3015,7 +3090,7 @@ mod tests { .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -3043,12 +3118,13 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -3086,6 +3162,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; #[cfg(c_bindings)] use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; @@ -3104,7 +3181,7 @@ mod tests { let expected_payer_note = "❤️".repeat(85); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .chain(Network::Testnet) .unwrap() diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 4bf3161123d..11b0a2f8c31 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -336,7 +336,7 @@ mod tests { let nonce = Nonce([0u8; 16]); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let supported_conversion = TestCurrencyConversion; + let conversion = TestCurrencyConversion; let recipient_pubkey = { let secret_bytes = >::from_hex( @@ -360,12 +360,12 @@ mod tests { .description("A Mathematical Treatise".into()) .amount( Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 100 }, - &supported_conversion, + &conversion, ) .unwrap() .build_unchecked() // Override the payer metadata and signing pubkey to match the test vectors - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .payer_metadata(Metadata::Bytes(vec![0; 8])) .payer_signing_pubkey(payer_keys.public_key()) @@ -397,12 +397,13 @@ mod tests { let nonce = Nonce([0u8; 16]); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .payer_note("bar".into()) .build_unchecked(); @@ -424,6 +425,7 @@ mod tests { let nonce = Nonce([0u8; 16]); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let recipient_pubkey = { let secret_key = SecretKey::from_slice(&[41; 32]).unwrap(); @@ -434,7 +436,7 @@ mod tests { .amount_msats(100) .unwrap() .build_unchecked() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -459,6 +461,7 @@ mod tests { let nonce = Nonce([0u8; 16]); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let recipient_pubkey = { let secret_key = SecretKey::from_slice(&[41; 32]).unwrap(); @@ -469,7 +472,7 @@ mod tests { .amount_msats(100) .unwrap() .build_unchecked() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 72752009678..3d61a91fb08 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -799,20 +799,23 @@ macro_rules! request_invoice_derived_signing_pubkey { ($self: ident, $offer: exp pub fn request_invoice< 'a, 'b, #[cfg(not(c_bindings))] - T: secp256k1::Signing + T: secp256k1::Signing, + CC: CurrencyConversion, >( &'a $self, expanded_key: &ExpandedKey, nonce: Nonce, #[cfg(not(c_bindings))] secp_ctx: &'b Secp256k1, #[cfg(c_bindings)] secp_ctx: &'b Secp256k1, - payment_id: PaymentId - ) -> Result<$builder, Bolt12SemanticError> { + payment_id: PaymentId, + currency_conversion: &'a CC, + ) -> Result<$builder, Bolt12SemanticError> + { if $offer.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - let mut builder = <$builder>::deriving_signing_pubkey(&$offer, expanded_key, nonce, secp_ctx, payment_id); + let mut builder = <$builder>::deriving_signing_pubkey(&$offer, expanded_key, nonce, secp_ctx, payment_id, currency_conversion); if let Some(hrn) = $hrn { #[cfg(c_bindings)] { @@ -829,7 +832,7 @@ macro_rules! request_invoice_derived_signing_pubkey { ($self: ident, $offer: exp #[cfg(not(c_bindings))] impl Offer { - request_invoice_derived_signing_pubkey!(self, self, InvoiceRequestBuilder<'a, 'b, T>, None); + request_invoice_derived_signing_pubkey!(self, self, InvoiceRequestBuilder<'a, 'b, T, CC>, None); } #[cfg(not(c_bindings))] @@ -837,7 +840,7 @@ impl OfferFromHrn { request_invoice_derived_signing_pubkey!( self, self.offer, - InvoiceRequestBuilder<'a, 'b, T>, + InvoiceRequestBuilder<'a, 'b, T, CC>, Some(self.hrn) ); } @@ -847,7 +850,7 @@ impl Offer { request_invoice_derived_signing_pubkey!( self, self, - InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>, + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC>, None ); } @@ -857,7 +860,7 @@ impl OfferFromHrn { request_invoice_derived_signing_pubkey!( self, self.offer, - InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>, + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC>, Some(self.hrn) ); } @@ -944,28 +947,48 @@ impl OfferContents { self.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) } - pub(super) fn check_amount_msats_for_quantity( - &self, amount_msats: Option, quantity: Option, + pub(super) fn check_amount_msats_for_quantity( + &self, currency_conversion: &CC, requested_amount_msats: Option, + requested_quantity: Option, ) -> Result<(), Bolt12SemanticError> { - let offer_amount_msats = match self.amount { - None => 0, - Some(Amount::Bitcoin { amount_msats }) => amount_msats, - Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency), - }; + // If the offer expects a quantity but none has been provided yet, + // the implied total amount cannot be determined. Defer amount + // validation until the quantity is known. + if self.expects_quantity() && requested_quantity.is_none() { + return Ok(()); + } - if !self.expects_quantity() || quantity.is_some() { - let expected_amount_msats = offer_amount_msats - .checked_mul(quantity.unwrap_or(1)) - .ok_or(Bolt12SemanticError::InvalidAmount)?; - let amount_msats = amount_msats.unwrap_or(expected_amount_msats); + let quantity = requested_quantity.unwrap_or(1); - if amount_msats < expected_amount_msats { - return Err(Bolt12SemanticError::InsufficientAmount); - } + let expected_amount_msats = self + .resolve_offer_amount(currency_conversion)? + .map(|unit_msats| { + unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount) + }) + .transpose()?; - if amount_msats > MAX_VALUE_MSAT { - return Err(Bolt12SemanticError::InvalidAmount); - } + let total_amount_msats = match (requested_amount_msats, expected_amount_msats) { + // The payer specified an amount and the offer defines a minimum. + // Enforce that the requested amount satisfies the minimum. + (Some(requested), Some(minimum)) if requested < minimum => { + Err(Bolt12SemanticError::InsufficientAmount) + }, + + // The payer specified a valid amount which satisfies the offer minimum + // (or the offer does not define one). + (Some(requested), _) => Ok(requested), + + // The payer did not specify an amount but the offer defines one. + // Use the offer-implied amount. + (None, Some(amount_msats)) => Ok(amount_msats), + + // Neither the payer nor the offer defines an amount. + (None, None) => Err(Bolt12SemanticError::MissingAmount), + }?; + + // Sanity check: + if total_amount_msats > MAX_VALUE_MSAT { + return Err(Bolt12SemanticError::InvalidAmount); } Ok(()) @@ -1561,6 +1584,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; @@ -1573,7 +1597,7 @@ mod tests { assert_eq!(offer.issuer_signing_pubkey(), Some(node_id)); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1584,7 +1608,7 @@ mod tests { // Fails verification when using the wrong method let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1601,7 +1625,7 @@ mod tests { let invoice_request = Offer::try_from(encoded_offer) .unwrap() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1617,7 +1641,7 @@ mod tests { let invoice_request = Offer::try_from(encoded_offer) .unwrap() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1632,6 +1656,7 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; let blinded_path = BlindedMessagePath::from_blinded_path( pubkey(40), @@ -1654,7 +1679,7 @@ mod tests { assert_ne!(offer.issuer_signing_pubkey(), Some(node_id)); let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1665,7 +1690,7 @@ mod tests { // Fails verification when using the wrong method let invoice_request = offer - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1680,7 +1705,7 @@ mod tests { let invoice_request = Offer::try_from(encoded_offer) .unwrap() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1698,7 +1723,7 @@ mod tests { let invoice_request = Offer::try_from(encoded_offer) .unwrap() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_and_sign() .unwrap(); @@ -1924,11 +1949,12 @@ mod tests { let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; match OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build() - .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnknownRequiredFeatures), diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index c0fd9dfdd3e..f5abd4a17dd 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -666,8 +666,11 @@ impl Refund { #[cfg(c_bindings)] impl Refund { - respond_with_explicit_signing_pubkey_methods!(self, InvoiceWithExplicitSigningPubkeyBuilder); - respond_with_derived_signing_pubkey_methods!(self, InvoiceWithDerivedSigningPubkeyBuilder); + respond_with_explicit_signing_pubkey_methods!( + self, + InvoiceWithExplicitSigningPubkeyBuilder<'_> + ); + respond_with_derived_signing_pubkey_methods!(self, InvoiceWithDerivedSigningPubkeyBuilder<'_>); } #[cfg(test)] From acf82c36dae37c579942c8229d1eb02a7cf52038 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 28 Mar 2026 19:40:50 +0530 Subject: [PATCH 6/8] [test] Add parsing coverage for explicit fiat request amounts Extend invoice request parsing tests for currency-denominated offers to cover explicit serialized msat amounts. Verify that well-formed explicit amounts still parse and out-of-range values are rejected. Co-Authored-By: OpenAI Codex --- lightning/src/offers/invoice_request.rs | 53 +++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 3ebfed85a65..2cbca7d1df3 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -2573,6 +2573,59 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .description("foo".to_string()) + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &conversion, + ) + .unwrap() + .build_unchecked() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .amount_msats(1_000_000) + .unwrap() + .build_unchecked_and_sign(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + // Parsing still accepts explicit amounts for currency-denominated offers when + // the serialized msat amount itself is well-formed. + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request with explicit amount: {:?}", e); + } + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .description("foo".to_string()) + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &conversion, + ) + .unwrap() + .build_unchecked() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .amount_msats_unchecked(MAX_VALUE_MSAT + 1) + .build_unchecked_and_sign(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .unwrap() From d15febfb96e6a2b8e2a978e0025754af8d69aa28 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 3 Apr 2026 15:01:45 +0530 Subject: [PATCH 7/8] [feat] Quote currency-denominated offer amounts in invoices Use CurrencyConversion while responding to an InvoiceRequest. This lets payees resolve currency-denominated offer amounts into the millisatoshi amount carried by the invoice. Requests that omit amount_msats remain omitted, and the invoice preserves the original request bytes instead of normalizing them into an explicit request amount. Deterministically-resolvable omitted request amounts are still validated before signing. On the payer side, the returned invoice amount is checked against the locally expected amount before payment starts. Receiver-side lower-bound checks still apply only to explicit payer-provided amounts, and async static invoices bind their payment secrets to the resolved offer amount when one exists. Co-Authored-By: OpenAI Codex --- fuzz/src/invoice_request_deser.rs | 5 +- lightning/src/ln/async_payments_tests.rs | 5 +- lightning/src/ln/channelmanager.rs | 66 +++++-- lightning/src/ln/offers_tests.rs | 3 +- lightning/src/ln/outbound_payment.rs | 85 +++++--- lightning/src/offers/flow.rs | 10 +- lightning/src/offers/invoice.rs | 241 ++++++++++++++--------- lightning/src/offers/invoice_request.rs | 79 +++++--- 8 files changed, 323 insertions(+), 171 deletions(-) diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index 7519cf5310a..52a76ba57a1 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -153,7 +153,10 @@ fn build_response( .unwrap(); let payment_hash = PaymentHash([42; 32]); - invoice_request.respond_with(vec![payment_path], payment_hash)?.build() + let conversion = FuzzCurrencyConversion; + invoice_request + .respond_with(&conversion, vec![payment_path], payment_hash)? + .build() } pub fn invoice_request_deser_test(data: &[u8], out: Out) { diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 6abcd63fd85..4bb9f76071a 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -60,7 +60,6 @@ use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::config::{HTLCInterceptionFlags, UserConfig}; use crate::util::ser::Writeable; -use crate::util::test_utils::TestCurrencyConversion; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1; @@ -1457,8 +1456,6 @@ fn amount_doesnt_match_invreq() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); - let conversion = TestCurrencyConversion; - nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); let release_held_htlc_om_3_0 = pass_async_payments_oms( static_invoice, @@ -1482,7 +1479,7 @@ fn amount_doesnt_match_invreq() { Nonce::from_entropy_source(nodes[0].keys_manager), &secp_ctx, payment_id, - &conversion, + nodes[0].node.currency_conversion, ) .unwrap() .amount_msats(amt_msat + 1) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d631c68a05b..89d7f060826 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5760,6 +5760,8 @@ impl< fn send_payment_for_verified_bolt12_invoice( &self, invoice: &Bolt12Invoice, payment_id: PaymentId, ) -> Result<(), Bolt12PaymentError> { + self.check_bolt12_invoice_amount(invoice, payment_id)?; + let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); let features = self.bolt12_invoice_features(); @@ -5781,6 +5783,31 @@ impl< ) } + fn check_bolt12_invoice_amount( + &self, invoice: &Bolt12Invoice, payment_id: PaymentId, + ) -> Result<(), Bolt12PaymentError> { + let (_, _, invoice_request) = self + .pending_outbound_payments + .invoice_request_for_pending_bolt12_invoice(payment_id) + .map_err(|e| match e { + Bolt12PaymentError::DuplicateInvoice => Bolt12PaymentError::DuplicateInvoice, + _ => Bolt12PaymentError::UnexpectedInvoice, + })?; + if let Some(ref invoice_request) = invoice_request { + let requested_amount_msats = invoice_request + .amount_msats(&self.currency_conversion) + .map_err(|_| Bolt12PaymentError::UnexpectedInvoice)?; + // A returned invoice quotes the amount the payee expects to receive. Make + // sure it matches the payer's locally expected amount before recording the + // invoice as received or initiating payment. + if invoice.amount_msats() != requested_amount_msats { + return Err(Bolt12PaymentError::UnexpectedInvoice); + } + } + + Ok(()) + } + fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) { let peers = self.get_peers_for_blinded_path(); let channels = self.list_usable_channels(); @@ -8554,26 +8581,23 @@ impl< }); let verified_invreq = match verify_opt { Some(verified_invreq) => { - match verified_invreq - .amount_msats(&self.flow.currency_conversion) - { - Ok(invreq_amt_msat) => { - if payment_data.total_msat < invreq_amt_msat { + if verified_invreq.has_amount_msats() { + // Only explicit payer-provided amounts act as a lower + // bound here. Omitted amounts are resolved into the + // invoice amount when the payee creates the invoice. + match verified_invreq + .amount_msats(&DefaultCurrencyConversion) + { + Ok(invreq_amt_msat) => { + if payment_data.total_msat < invreq_amt_msat { + fail_htlc!(claimable_htlc, payment_hash); + } + }, + Err(_) => { + debug_assert!(false); fail_htlc!(claimable_htlc, payment_hash); - } - }, - Err(_) => { - // `amount_msats()` can only fail if the invoice request does not specify an amount - // and the underlying offer's amount cannot be resolved. - // - // This invoice request corresponds to an offer we constructed, and we only allow - // creating offers with currency amounts that the node explicitly supports. - // - // Therefore, amount resolution must succeed here. Reaching this branch indicates - // an internal logic error. - debug_assert!(false); - fail_htlc!(claimable_htlc, payment_hash); - }, + }, + } } verified_invreq }, @@ -17068,6 +17092,10 @@ impl< ); if self.config.read().unwrap().manually_handle_bolt12_invoices { + if let Err(e) = self.check_bolt12_invoice_amount(&invoice, payment_id) { + handle_pay_invoice_res!(Err(e), invoice, logger); + } + // Update the corresponding entry in `PendingOutboundPayment` for this invoice. // This ensures that event generation remains idempotent in case we receive // the same invoice multiple times. diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index d4bc3bc9edb..f3243123732 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2367,6 +2367,7 @@ fn fails_paying_invoice_with_unknown_required_features() { .build(); let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; david.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); connect_peers(david, bob); @@ -2401,7 +2402,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let invoice = match verified_invoice_request { InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => { - request.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap() + request.respond_using_derived_keys_no_std(&conversion, 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 2766cc9c355..908fbab94a3 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -24,6 +24,7 @@ use crate::ln::channelmanager::{ use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; +use crate::offers::currency::CurrencyConversion; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; @@ -92,6 +93,9 @@ pub(crate) enum PendingOutboundPayment { InvoiceReceived { payment_hash: PaymentHash, retry_strategy: Retry, + // Preserve the originating invoice request so offer-based amount validation + // can still happen if invoice sending is deferred until after receipt. + invoice_request: Option, // Currently unused, but replicated from `AwaitingInvoice` to avoid potential // race conditions where this field might be missing upon reload. It may be required // for future retries. @@ -1104,8 +1108,7 @@ impl OutboundPayments { IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, { - - let (payment_hash, retry_strategy, params_config, _) = self + let (payment_hash, retry_strategy, params_config, _invoice_request, _) = self .mark_invoice_received_and_get_details(invoice, payment_id)?; if invoice.invoice_features().requires_unknown_bits_from(&features) { @@ -1237,9 +1240,10 @@ impl OutboundPayments { Ok(()) } - pub(super) fn static_invoice_received( - &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, - best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES, + pub(super) fn static_invoice_received<'a, ES: EntropySource, CC: CurrencyConversion>( + &'a self, invoice: &StaticInvoice, currency_conversion: &'a CC, payment_id: PaymentId, + features: Bolt12InvoiceFeatures, best_block_height: u32, duration_since_epoch: Duration, + entropy_source: ES, pending_events: &Mutex)>>, ) -> Result<(), Bolt12PaymentError> { macro_rules! abandon_with_entry { @@ -1285,18 +1289,11 @@ impl OutboundPayments { )); } - let amount_msat = match InvoiceBuilder::::amount_msats( + let amount_msat = InvoiceBuilder::::amount_msats( invreq, - ) { - Ok(amt) => amt, - Err(_) => { - // We check this during invoice request parsing, when constructing the invreq's - // contents from its TLV stream. - debug_assert!(false, "LDK requires an msat amount in either the invreq or the invreq's underlying offer"); - abandon_with_entry!(entry, PaymentFailureReason::UnexpectedError); - return Err(Bolt12PaymentError::UnknownRequiredFeatures); - }, - }; + currency_conversion, + ) + .map_err(|_| Bolt12PaymentError::UnexpectedInvoice)?; let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes()); let payment_hash = @@ -2057,12 +2054,37 @@ impl OutboundPayments { } } + pub(super) fn invoice_request_for_pending_bolt12_invoice( + &self, payment_id: PaymentId, + ) -> Result<(Retry, RouteParametersConfig, Option), Bolt12PaymentError> { + match self.pending_outbound_payments.lock().unwrap().get(&payment_id) { + Some(PendingOutboundPayment::AwaitingInvoice { + retry_strategy, + route_params_config, + retryable_invoice_request, + .. + }) => Ok(( + *retry_strategy, + *route_params_config, + retryable_invoice_request.as_ref().map(|invreq| invreq.invoice_request.clone()), + )), + Some(PendingOutboundPayment::InvoiceReceived { + retry_strategy, + route_params_config, + invoice_request, + .. + }) => Ok((*retry_strategy, *route_params_config, invoice_request.clone())), + Some(_) => Err(Bolt12PaymentError::DuplicateInvoice), + None => Err(Bolt12PaymentError::UnexpectedInvoice), + } + } + #[rustfmt::skip] pub(super) fn mark_invoice_received( &self, invoice: &Bolt12Invoice, payment_id: PaymentId ) -> Result<(), Bolt12PaymentError> { self.mark_invoice_received_and_get_details(invoice, payment_id) - .and_then(|(_, _, _, is_newly_marked)| { + .and_then(|(_, _, _, _, is_newly_marked)| { is_newly_marked .then_some(()) .ok_or(Bolt12PaymentError::DuplicateInvoice) @@ -2072,31 +2094,43 @@ impl OutboundPayments { #[rustfmt::skip] fn mark_invoice_received_and_get_details( &self, invoice: &Bolt12Invoice, payment_id: PaymentId - ) -> Result<(PaymentHash, Retry, RouteParametersConfig, bool), Bolt12PaymentError> { + ) -> Result<(PaymentHash, Retry, RouteParametersConfig, Option, bool), Bolt12PaymentError> { match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::AwaitingInvoice { - retry_strategy: retry, route_params_config, .. + retry_strategy: retry, route_params_config, retryable_invoice_request, .. } => { let payment_hash = invoice.payment_hash(); let retry = *retry; let config = *route_params_config; + let invoice_request = + retryable_invoice_request.as_ref().map(|invreq| invreq.invoice_request.clone()); *entry.into_mut() = PendingOutboundPayment::InvoiceReceived { payment_hash, retry_strategy: retry, + invoice_request: invoice_request.clone(), route_params_config: config, }; - Ok((payment_hash, retry, config, true)) + Ok((payment_hash, retry, config, invoice_request, true)) }, // When manual invoice handling is enabled, the corresponding `PendingOutboundPayment` entry // is already updated at the time the invoice is received. This ensures that `InvoiceReceived` // event generation remains idempotent, even if the same invoice is received again before the // event is handled by the user. PendingOutboundPayment::InvoiceReceived { - retry_strategy, route_params_config, .. + payment_hash, retry_strategy, route_params_config, invoice_request, } => { - Ok((invoice.payment_hash(), *retry_strategy, *route_params_config, false)) + if *payment_hash != invoice.payment_hash() { + return Err(Bolt12PaymentError::DuplicateInvoice); + } + Ok(( + *payment_hash, + *retry_strategy, + *route_params_config, + invoice_request.clone(), + false, + )) }, _ => Err(Bolt12PaymentError::DuplicateInvoice), }, @@ -2811,6 +2845,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, |fee_msat| RouteParametersConfig::default().with_max_total_routing_fee_msat(fee_msat) ) ))), + (7, invoice_request, option), }, // Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because no // HTLCs are in-flight. @@ -3261,7 +3296,7 @@ mod tests { .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap() + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), created_at).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3311,7 +3346,7 @@ mod tests { .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3377,7 +3412,7 @@ mod tests { .build() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(&conversion, 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 b617b431ed3..3eac39c1d31 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -43,7 +43,7 @@ use crate::offers::invoice_request::{ InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer, VerifiedInvoiceRequest, }; use crate::offers::nonce::Nonce; -use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder}; +use crate::offers::offer::{DerivedMetadata, Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; @@ -822,8 +822,9 @@ impl OffersMessageFlow { /// by the [`OffersMessageFlow`], and any corresponding [`Bolt12Invoice`] received in response /// can be verified using [`Self::verify_bolt12_invoice`]. /// - /// The provided [`CurrencyConversion`] is used to resolve any currency-denominated offer amount - /// while building the request. + /// The provided [`CurrencyConversion`] is retained on the [`InvoiceRequest`] so + /// currency-denominated offer amounts can later be resolved when + /// [`InvoiceRequest::amount_msats`] is evaluated. /// /// # Nonce /// The nonce is used to create a unique [`InvoiceRequest::payer_metadata`] for the invoice request. @@ -1715,9 +1716,10 @@ impl OffersMessageFlow { // Set the invoice to expire at the same time as the offer. We aim to update this invoice as // often as possible, so there shouldn't be any reason to have it expire earlier than the // offer. + let amount_msat = offer.resolve_offer_amount(currency_conversion).map_err(|_| ())?; let payment_secret = inbound_payment::create_for_spontaneous_payment( expanded_key, - None, // The async receive offers we create are always amount-less + amount_msat, offer_relative_expiry, duration_since_epoch.as_secs(), None, diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 9f9f57501b8..aac203443c6 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -23,6 +23,7 @@ //! use bitcoin::hashes::Hash; //! use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; //! use core::convert::TryFrom; +//! use lightning::offers::currency::DefaultCurrencyConversion; //! use lightning::offers::invoice::UnsignedBolt12Invoice; //! use lightning::offers::invoice_request::InvoiceRequest; //! use lightning::offers::refund::Refund; @@ -36,6 +37,7 @@ //! # fn create_payment_hash() -> PaymentHash { unimplemented!() } //! # //! # fn parse_invoice_request(bytes: Vec) -> Result<(), lightning::offers::parse::Bolt12ParseError> { +//! let conversion = DefaultCurrencyConversion; //! let payment_paths = create_payment_paths(); //! let payment_hash = create_payment_hash(); //! let secp_ctx = Secp256k1::new(); @@ -50,13 +52,13 @@ #![cfg_attr( feature = "std", doc = " - .respond_with(payment_paths, payment_hash)? + .respond_with(&conversion, payment_paths, payment_hash)? " )] #![cfg_attr( not(feature = "std"), doc = " - .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? + .respond_with_no_std(&conversion, payment_paths, payment_hash, core::time::Duration::from_secs(0))? " )] //! # ) @@ -120,8 +122,8 @@ use crate::blinded_path::BlindedPath; use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; -use crate::ln::msgs::DecodeError; -use crate::offers::currency::DefaultCurrencyConversion; +use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::currency::{CurrencyConversion, DefaultCurrencyConversion}; #[cfg(test)] use crate::offers::invoice_macros::invoice_builder_methods_test_common; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; @@ -242,11 +244,12 @@ impl SigningPubkeyStrategy for DerivedSigningPubkey {} 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, + pub(super) fn for_offer( + invoice_request: &'a InvoiceRequest, currency_conversion: &CC, + 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, currency_conversion)?; let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -314,11 +317,12 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { 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, + pub(super) fn for_offer_using_keys( + invoice_request: &'a InvoiceRequest, currency_conversion: &CC, + 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, currency_conversion)?; let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), @@ -394,19 +398,40 @@ 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, + pub(crate) fn amount_msats( + invoice_request: &InvoiceRequest, currency_conversion: &CC, ) -> 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 requested_msats = invoice_request.amount_msats(currency_conversion)?; + + // Only requests with an explicit `amount_msats` need a separate minimum check. + // When the request omits the amount, `invoice_request.amount_msats(...)` has + // already resolved the offer amount for this quantity, so checking the minimum + // here would repeat the currency conversion and could observe a different + // conversion snapshot. + if invoice_request.has_amount_msats() { + let quantity = invoice_request.quantity().unwrap_or(1); + let minimum_offer_msats = + match invoice_request.resolve_offer_amount(currency_conversion)? { + Some(unit_msats) => Some( + unit_msats + .checked_mul(quantity) + .ok_or(Bolt12SemanticError::InvalidAmount)?, + ), + None => None, + }; + + if let Some(minimum) = minimum_offer_msats { + if requested_msats < minimum { + return Err(Bolt12SemanticError::InsufficientAmount); + } + } } + + if requested_msats > MAX_VALUE_MSAT { + return Err(Bolt12SemanticError::InvalidAmount); + } + + Ok(requested_msats) } #[cfg_attr(c_bindings, allow(dead_code))] @@ -642,14 +667,12 @@ impl UnsignedBolt12Invoice { record.write(&mut bytes).unwrap(); } - let remaining_bytes = &invreq_bytes[bytes.len()..]; - invoice_tlv_stream.write(&mut bytes).unwrap(); const EXPERIMENTAL_TLV_ALLOCATION_SIZE: usize = 0; let mut experimental_bytes = Vec::with_capacity(EXPERIMENTAL_TLV_ALLOCATION_SIZE); - let experimental_tlv_stream = TlvStream::new(remaining_bytes).range(EXPERIMENTAL_TYPES); + let experimental_tlv_stream = TlvStream::new(invreq_bytes).range(EXPERIMENTAL_TYPES); for record in experimental_tlv_stream { record.write(&mut experimental_bytes).unwrap(); } @@ -1770,34 +1793,23 @@ impl TryFrom for InvoiceContents { experimental_invoice_request_tlv_stream, ))?; - // Note: - // It is safe to use `DefaultCurrencyConversion` here. - // - // `amount_msats()` can fail only if: - // 1. The computed payable amount exceeds the maximum lightning-payable amount, or - // 2. The invoice request has no explicit amount while the underlying offer - // is currency-denominated. - // - // Neither can occur here: - // - Invalid payable amounts are rejected earlier. - // - For currency-denominated offers we always set an explicit amount - // when constructing the invoice request. - // - // Since this invoice corresponds to the invoice request we created, - // `amount_msats()` must succeed here. - let requested_amount_msats = - match invoice_request.amount_msats(&DefaultCurrencyConversion) { - Ok(msats) => msats, - Err(_) => { - debug_assert!( - false, - "amount_msats must succeed for invoice requests constructed by LDK" - ); + // If the invoice request omits `amount_msats`, parse-time validation can only + // recompute the expected amount for offers whose amount is already in msats. + // We therefore use `DefaultCurrencyConversion` here and deliberately tolerate + // `UnsupportedCurrency` so fiat-denominated offers can still parse. Those + // omitted-amount fiat invoices are validated later in the payment flow, where + // a real `CurrencyConversion` is available. + match invoice_request.amount_msats(&DefaultCurrencyConversion) { + Ok(requested_amount_msats) => { + if amount_msats != requested_amount_msats { return Err(Bolt12SemanticError::InvalidAmount); - }, - }; + } + }, + Err(Bolt12SemanticError::UnsupportedCurrency) => (), + Err(e) => return Err(e), + } - if amount_msats != requested_amount_msats { + if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } @@ -1877,7 +1889,7 @@ mod tests { use crate::offers::merkle::{self, SignError, SignatureTlvStreamRef, TaggedHash, TlvStream}; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity, + Amount, CurrencyCode, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; @@ -1932,7 +1944,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths.clone(), payment_hash, now) + .respond_with_no_std(&conversion, payment_paths.clone(), payment_hash, now) .unwrap() .build() .unwrap(); @@ -2079,6 +2091,44 @@ mod tests { } } + #[test] + fn parses_invoice_for_fiat_offer_without_explicit_request_amount() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + + let invoice = OfferBuilder::new(recipient_pubkey()) + .amount( + Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }, + &conversion, + ) + .unwrap() + .build() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .build_and_sign() + .unwrap() + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + let parsed_invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); + let (_, _, invoice_request_tlv_stream, invoice_tlv_stream, _, _, _, _) = + parsed_invoice.as_tlv_stream(); + assert_eq!(invoice_request_tlv_stream.amount, None); + assert_eq!(invoice_tlv_stream.amount, Some(10_000)); + assert_eq!(parsed_invoice.amount_msats(), 10_000); + } + #[test] fn builds_invoice_for_refund_with_defaults() { let payment_paths = payment_paths(); @@ -2204,7 +2254,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with(payment_paths(), payment_hash()) + .respond_with(&conversion, payment_paths(), payment_hash()) .unwrap() .build() { @@ -2219,7 +2269,7 @@ mod tests { .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) .unwrap() .build_unchecked_and_sign() - .respond_with(payment_paths(), payment_hash()) + .respond_with(&conversion, payment_paths(), payment_hash()) .unwrap() .build() { @@ -2301,7 +2351,12 @@ mod tests { match verified_request { InvoiceRequestVerifiedFromOffer::DerivedKeys(req) => { let invoice = req - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) + .respond_using_derived_keys_no_std( + &conversion, + payment_paths(), + payment_hash(), + now(), + ) .unwrap() .build_and_sign(&secp_ctx); @@ -2404,7 +2459,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now) .unwrap() .relative_expiry(one_hour.as_secs() as u32) .build() @@ -2425,7 +2480,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now - one_hour) .unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build() @@ -2458,7 +2513,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2489,7 +2544,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2509,7 +2564,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(&conversion, payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), @@ -2538,7 +2593,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -2595,7 +2650,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2624,7 +2679,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2642,7 +2697,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2670,7 +2725,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2748,7 +2803,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2793,7 +2848,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .relative_expiry(3600) .build() @@ -2827,7 +2882,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2872,7 +2927,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2915,7 +2970,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2959,11 +3014,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(&conversion, 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(&conversion, payment_paths(), payment_hash(), now()) + .unwrap(); let invoice_builder = invoice_builder .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -3023,7 +3080,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3112,6 +3169,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &conversion, payment_paths(), payment_hash(), now(), @@ -3142,6 +3200,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &conversion, payment_paths(), payment_hash(), now(), @@ -3184,7 +3243,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3213,7 +3272,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3278,7 +3337,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3312,7 +3371,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3356,7 +3415,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3395,7 +3454,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3442,7 +3501,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -3468,7 +3527,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3509,7 +3568,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3547,7 +3606,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3589,7 +3648,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3625,7 +3684,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3674,7 +3733,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3721,7 +3780,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths, payment_hash(), now) + .respond_with_no_std(&conversion, 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 2cbca7d1df3..8c44bf800e7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -51,7 +51,7 @@ //! let mut buffer = Vec::new(); //! //! # use lightning::offers::invoice_request::InvoiceRequestBuilder; -//! # >::from( +//! # >::from( //! "lno1qcp4256ypq" //! .parse::()? //! .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion)? @@ -286,9 +286,25 @@ macro_rules! invoice_request_builder_methods { ( } $self.invoice_request.offer.check_quantity($self.invoice_request.quantity)?; - if let Some(amount_msats) = $self.invoice_request.amount_msats { - // Omitted amounts are resolved when the payee builds the invoice, so only - // explicit payer-provided amounts need request-time validation here. + let amount_msats = match $self.invoice_request.amount_msats { + Some(amount_msats) => Some(amount_msats), + None => match $self.invoice_request.offer.resolve_offer_amount($self.currency_conversion) { + Ok(Some(unit_msats)) => { + let quantity = $self.invoice_request.quantity.unwrap_or(1); + Some( + unit_msats + .checked_mul(quantity) + .filter(|amount_msats| *amount_msats <= MAX_VALUE_MSAT) + .ok_or(Bolt12SemanticError::InvalidAmount)?, + ) + }, + Ok(None) | Err(Bolt12SemanticError::UnsupportedCurrency) => None, + Err(err) => return Err(err), + }, + }; + if let Some(amount_msats) = amount_msats { + // Preserve the omitted amount on the wire, while still rejecting requests whose + // effective amount is already deterministically invalid at signing time. $self.invoice_request.offer.check_amount_msats_for_quantity( $self.currency_conversion, Some(amount_msats), $self.invoice_request.quantity )?; @@ -791,14 +807,15 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( /// /// [`Duration`]: core::time::Duration #[cfg(feature = "std")] - pub fn respond_with( - &$self, payment_paths: Vec, payment_hash: PaymentHash - ) -> Result<$builder, Bolt12SemanticError> { + pub fn respond_with( + &$self, currency_conversion: &CC, 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"); - $contents.respond_with_no_std(payment_paths, payment_hash, created_at) + $contents.respond_with_no_std(currency_conversion, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request with the given required fields. @@ -826,10 +843,11 @@ 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, + pub fn respond_with_no_std( + &$self, currency_conversion: &CC, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration - ) -> Result<$builder, Bolt12SemanticError> { + ) -> Result<$builder, Bolt12SemanticError> + { if $contents.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -839,22 +857,23 @@ 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, currency_conversion, 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, + pub(super) fn respond_with_no_std_using_signing_pubkey( + &$self, currency_conversion: &CC, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration, signing_pubkey: PublicKey - ) -> Result<$builder, Bolt12SemanticError> { + ) -> Result<$builder, Bolt12SemanticError> + { debug_assert!($contents.contents.inner.offer.issuer_signing_pubkey().is_none()); if $contents.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) + <$builder>::for_offer(&$contents, currency_conversion, payment_paths, created_at, payment_hash, signing_pubkey) } } } @@ -1023,14 +1042,15 @@ 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 - ) -> Result<$builder, Bolt12SemanticError> { + pub fn respond_using_derived_keys( + &$self, currency_conversion: &CC, 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(currency_conversion, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses @@ -1040,10 +1060,11 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// See [`InvoiceRequest::respond_with_no_std`] for further details. /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - pub fn respond_using_derived_keys_no_std( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + pub fn respond_using_derived_keys_no_std( + &$self, currency_conversion: &CC, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration - ) -> Result<$builder, Bolt12SemanticError> { + ) -> Result<$builder, Bolt12SemanticError> + { if $self.inner.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -1056,7 +1077,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, currency_conversion, payment_paths, created_at, payment_hash, keys ) } } } @@ -1493,6 +1514,12 @@ impl TryFrom for InvoiceRequestContents { offer.check_quantity(quantity)?; + if let Some(amount_msats) = amount { + if amount_msats > MAX_VALUE_MSAT { + return Err(Bolt12SemanticError::InvalidAmount); + } + } + match offer.check_amount_msats_for_quantity(&DefaultCurrencyConversion, amount, quantity) { // If the offer amount is currency-denominated, we intentionally skip the // amount check here, as currency conversion is not available at this stage. @@ -1776,7 +1803,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -2391,7 +2418,7 @@ mod tests { .features_unchecked(InvoiceRequestFeatures::unknown()) .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_std(&conversion, payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnknownRequiredFeatures), From 13c19b102f96e5487a9fce17c37a76424a3dc3c9 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 3 Apr 2026 15:01:54 +0530 Subject: [PATCH 8/8] [test] Cover currency-denominated invoice handling Add targeted tests for the currency-denominated offer and invoice handling paths introduced by the new conversion flow. Cover end-to-end offer flows, parsed fiat request amounts below an offer minimum, deferred invoice replay identity, and async amount resolution failures. This keeps the invoice-quoting change pinned across its success and rejection paths without spreading closely related test updates across multiple commits. Co-Authored-By: OpenAI Codex --- lightning/src/ln/offers_tests.rs | 234 ++++++++++++++++++++++++ lightning/src/ln/outbound_payment.rs | 170 ++++++++++++++++- lightning/src/offers/invoice_request.rs | 40 ++++ 3 files changed, 438 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index f3243123732..15f2cbfd726 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -52,6 +52,7 @@ use crate::blinded_path::message::OffersContext; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self}; use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry}; +use crate::offers::offer::{Amount, CurrencyCode}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; @@ -66,6 +67,7 @@ use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig}; use crate::sign::{NodeSigner, Recipient}; +use crate::types::payment::PaymentHash; use crate::util::ser::Writeable; /// This used to determine whether we built a compact path or not, but now its just a random @@ -73,6 +75,7 @@ use crate::util::ser::Writeable; const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); use crate::prelude::*; +use crate::offers::test_utils::{payment_hash, payment_paths}; use crate::util::test_utils::TestCurrencyConversion; macro_rules! expect_recent_payment { @@ -916,6 +919,78 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } +/// Checks that an offer can be paid through a one-hop blinded path and that ephemeral pubkeys are +/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the +/// introduction node of the blinded path. +#[test] +fn creates_and_pays_for_offer_with_fiat_amount_using_one_hop_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let amount = Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }; + + let offer = alice.node + .create_offer_builder().unwrap() + .amount(amount, alice.node.currency_conversion).unwrap() + .build(); + assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert!(check_compact_path_introduction_node(&path, bob, alice_id)); + } + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + }); + assert_eq!(invoice_request.amount_msats(alice.node.currency_conversion), Ok(1_000_000)); + assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); + assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH)); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, reply_path) = extract_invoice(bob, &onion_message); + assert_eq!(invoice.amount_msats(), 1_000_000); + assert_ne!(invoice.signing_pubkey(), alice_id); + assert!(!invoice.payment_paths().is_empty()); + for path in invoice.payment_paths() { + assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + } + assert!(check_dummy_hopped_path_length(&reply_path, bob, alice_id, DUMMY_HOPS_PATH_LENGTH)); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + /// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are /// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the /// introduction node of the blinded path. @@ -1400,6 +1475,165 @@ fn pays_bolt12_invoice_asynchronously() { ); } +/// Checks that a deferred fiat-denominated invoice is rejected if its quoted msat amount does not +/// match the payer's local conversion result. +#[test] +fn rejects_unexpected_fiat_bolt12_invoice_amount_asynchronously() { + let mut manually_pay_cfg = test_default_channel_config(); + manually_pay_cfg.manually_handle_bolt12_invoices = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + let conversion = TestCurrencyConversion; + + let offer = alice.node + .create_offer_builder().unwrap() + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &conversion, + ) + .unwrap() + .build(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + let nonce = extract_offer_nonce(alice, &onion_message); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let mut events = bob.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + + let (invoice, context) = match events.pop().unwrap() { + Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => { + assert_eq!(actual_payment_id, payment_id); + (invoice, context) + }, + _ => panic!("No Event::InvoiceReceived"), + }; + + let expanded_key = alice.keys_manager.get_expanded_key(); + let secp_ctx = Secp256k1::new(); + let verified_invoice_request = + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap(); + + let bad_invoice = match verified_invoice_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => request + .respond_using_derived_keys_no_std( + &conversion, + payment_paths(), + payment_hash(), + Duration::from_secs(1000), + ) + .unwrap() + .amount_msats_unchecked(invoice.amount_msats() + 1) + .build_and_sign(&secp_ctx) + .unwrap(), + InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => { + panic!("Expected invoice request with derived keys"); + }, + }; + + assert_eq!( + bob.node.send_payment_for_bolt12_invoice(&bad_invoice, context.as_ref()), + Err(Bolt12PaymentError::UnexpectedInvoice), + ); +} + +/// Checks that deferred manual invoice handling rejects a different invoice replayed for the same +/// payment id after the first invoice has already been recorded. +#[test] +fn rejects_different_bolt12_invoice_replayed_asynchronously() { + let mut manually_pay_cfg = test_default_channel_config(); + manually_pay_cfg.manually_handle_bolt12_invoices = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let offer = alice.node + .create_offer_builder().unwrap() + .amount_msats(10_000_000).unwrap() + .build(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + let nonce = extract_offer_nonce(alice, &onion_message); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let mut events = bob.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + + let (invoice, context) = match events.pop().unwrap() { + Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => { + assert_eq!(actual_payment_id, payment_id); + (invoice, context) + }, + _ => panic!("No Event::InvoiceReceived"), + }; + + let expanded_key = alice.keys_manager.get_expanded_key(); + let secp_ctx = Secp256k1::new(); + let verified_invoice_request = + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap(); + + let replayed_invoice = match verified_invoice_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => request + .respond_using_derived_keys_no_std( + alice.node.currency_conversion, + payment_paths(), + PaymentHash([43; 32]), + Duration::from_secs(1000), + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(), + InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => { + panic!("Expected invoice request with derived keys"); + }, + }; + + assert_ne!(replayed_invoice.payment_hash(), invoice.payment_hash()); + assert_eq!( + bob.node.send_payment_for_bolt12_invoice(&replayed_invoice, context.as_ref()), + Err(Bolt12PaymentError::DuplicateInvoice), + ); +} /// Checks that an offer can be created using an unannounced node as a blinded path's introduction /// node. This is only preferred if there are no other options which may indicated either the offer /// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 908fbab94a3..ff3c0a12315 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -92,6 +92,9 @@ pub(crate) enum PendingOutboundPayment { // Helps avoid holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding. InvoiceReceived { payment_hash: PaymentHash, + // Persist the invoice identity so duplicate delivery of the same invoice remains + // idempotent without accepting a different invoice that reuses the payment hash. + invoice_identity: Option<[u8; 32]>, retry_strategy: Retry, // Preserve the originating invoice request so offer-based amount validation // can still happen if invoice sending is deferred until after receipt. @@ -1289,11 +1292,18 @@ impl OutboundPayments { )); } - let amount_msat = InvoiceBuilder::::amount_msats( + let amount_msat = match InvoiceBuilder::::amount_msats( invreq, currency_conversion, - ) - .map_err(|_| Bolt12PaymentError::UnexpectedInvoice)?; + ) { + Ok(amount_msat) => amount_msat, + Err(_) => { + // An amount must be provided explicitly in the invoice request or + // remain derivable from the underlying offer at this stage. + abandon_with_entry!(entry, PaymentFailureReason::UnexpectedError); + return Err(Bolt12PaymentError::UnexpectedInvoice); + }, + }; let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes()); let payment_hash = @@ -2107,6 +2117,7 @@ impl OutboundPayments { retryable_invoice_request.as_ref().map(|invreq| invreq.invoice_request.clone()); *entry.into_mut() = PendingOutboundPayment::InvoiceReceived { payment_hash, + invoice_identity: Some(invoice.signable_hash()), retry_strategy: retry, invoice_request: invoice_request.clone(), route_params_config: config, @@ -2119,11 +2130,18 @@ impl OutboundPayments { // event generation remains idempotent, even if the same invoice is received again before the // event is handled by the user. PendingOutboundPayment::InvoiceReceived { - payment_hash, retry_strategy, route_params_config, invoice_request, + payment_hash, invoice_identity, retry_strategy, route_params_config, + invoice_request, } => { if *payment_hash != invoice.payment_hash() { return Err(Bolt12PaymentError::DuplicateInvoice); } + if invoice_identity + .as_ref() + .is_some_and(|invoice_identity| *invoice_identity != invoice.signable_hash()) + { + return Err(Bolt12PaymentError::DuplicateInvoice); + } Ok(( *payment_hash, *retry_strategy, @@ -2846,6 +2864,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, ) ))), (7, invoice_request, option), + (9, invoice_identity, option), }, // Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because no // HTLCs are in-flight. @@ -2898,13 +2917,15 @@ mod tests { use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::outbound_payment::{ Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, RecipientCustomTlvs, Retry, - RetryableSendFailure, StaleExpiration, + RetryableInvoiceRequest, RetryableSendFailure, StaleExpiration, }; + use crate::offers::currency::DefaultCurrencyConversion; #[cfg(feature = "std")] use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; - use crate::offers::offer::OfferBuilder; + use crate::offers::offer::{Amount, CurrencyCode, OfferBuilder}; + use crate::offers::static_invoice::StaticInvoiceBuilder; use crate::offers::test_utils::*; use crate::routing::gossip::NetworkGraph; use crate::routing::router::{ @@ -3598,4 +3619,141 @@ mod tests { reason: Some(PaymentFailureReason::UserAbandoned), }, None)); } + + #[test] + #[rustfmt::skip] + fn abandoning_async_payment_on_amount_resolution_failure() { + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(new_hash_map()); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let conversion = TestCurrencyConversion; + let unsupported_conversion = DefaultCurrencyConversion; + + let offer = OfferBuilder::deriving_signing_pubkey( + recipient_pubkey(), + &expanded_key, + nonce, + &secp_ctx, + ) + .path(blinded_path()) + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &conversion, + ) + .unwrap() + .build(); + + let invoice_request = offer + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .build_and_sign() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths(), + vec![blinded_path()], + now(), + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, + expiration, + Retry::Attempts(0), + RouteParametersConfig::default(), + Some(RetryableInvoiceRequest { + invoice_request, + nonce, + needs_retry: false, + }), + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + assert_eq!( + outbound_payments.static_invoice_received( + &invoice, + &unsupported_conversion, + payment_id, + Bolt12InvoiceFeatures::empty(), + 0, + now(), + &entropy, + &pending_events, + ), + Err(Bolt12PaymentError::UnexpectedInvoice), + ); + assert!(!outbound_payments.has_pending_payments()); + assert_eq!( + pending_events.lock().unwrap().pop_front(), + Some((Event::PaymentFailed { + payment_hash: None, + payment_id, + reason: Some(PaymentFailureReason::UnexpectedError), + }, None)), + ); + assert!(pending_events.lock().unwrap().is_empty()); + } + + #[test] + #[rustfmt::skip] + fn rejects_distinct_duplicate_invoice_with_same_payment_hash() { + let outbound_payments = OutboundPayments::new(new_hash_map()); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + let conversion = TestCurrencyConversion; + let invoice_request = dummy_invoice_request(); + let payment_hash = payment_hash(); + + let first_invoice = invoice_request + .respond_with_no_std(&conversion, payment_paths(), payment_hash, now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + let second_invoice = invoice_request + .respond_with_no_std( + &conversion, + payment_paths(), + payment_hash, + now() + Duration::from_secs(1), + ) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, + expiration, + Retry::Attempts(0), + RouteParametersConfig::default(), + None, + ).is_ok() + ); + + assert_eq!(outbound_payments.mark_invoice_received(&first_invoice, payment_id), Ok(())); + assert_eq!( + outbound_payments.mark_invoice_received(&second_invoice, payment_id), + Err(Bolt12PaymentError::DuplicateInvoice), + ); + } } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 8c44bf800e7..7ca016b5406 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -2676,6 +2676,46 @@ mod tests { } } + #[test] + fn responding_to_parsed_fiat_request_rejects_insufficient_explicit_amount() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let conversion = TestCurrencyConversion; + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .description("foo".to_string()) + .amount( + Amount::Currency { + iso4217_code: CurrencyCode::new(*b"USD").unwrap(), + amount: 1000, + }, + &conversion, + ) + .unwrap() + .build_unchecked() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion) + .unwrap() + .amount_msats_unchecked(999_999) + .build_unchecked_and_sign(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + let parsed_invoice_request = InvoiceRequest::try_from(buffer).unwrap(); + + match parsed_invoice_request.respond_with_no_std( + &conversion, + payment_paths(), + payment_hash(), + now(), + ) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::InsufficientAmount), + } + } + #[test] fn parses_invoice_request_with_quantity() { let expanded_key = ExpandedKey::new([42; 32]);