From d4f29d803c53703edd9df45144c0d5c3c53f2636 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 12 Feb 2026 19:12:53 +0530 Subject: [PATCH 1/6] Introduce CurrencyConversion trait Adds a `CurrencyConversion` trait allowing users to provide logic for converting currency-denominated amounts into millisatoshis. LDK itself cannot perform such conversions as exchange rates are external, time-dependent, and application-specific. Instead, the conversion logic must be supplied by the user. This trait forms the foundation for supporting Offers denominated in fiat currencies while keeping exchange-rate handling outside the core protocol logic. --- fuzz/src/invoice_request_deser.rs | 15 +++++++- lightning/src/offers/currency.rs | 63 +++++++++++++++++++++++++++++++ lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 16 ++++++++ 4 files changed, 95 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..6b38118d35e 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,18 @@ pub fn do_test(data: &[u8], _out: Out) { } } +struct FuzzCurrencyConversion; + +impl CurrencyConversion for FuzzCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result { + unreachable!() + } + + fn tolerance_percent(&self) -> u8 { + unreachable!() + } +} + 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..8e0214eff6f --- /dev/null +++ b/lightning/src/offers/currency.rs @@ -0,0 +1,63 @@ +// 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. +/// +/// This convention ensures amounts remain precise and purely integer-based when parsing and +/// validating BOLT12 invoice requests. +pub trait CurrencyConversion { + /// Returns the conversion rate in **msats per minor unit** for the given + /// ISO-4217 currency code. + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result; + + /// Returns the acceptable tolerance, expressed as a percentage, used when + /// deriving conversion ranges. + /// + /// This represents a user-level policy (e.g., allowance for exchange-rate + /// drift or cached data) and does not directly affect fiat-to-msat conversion + /// outside of range computation. + fn tolerance_percent(&self) -> u8; +} + +impl> CurrencyConversion for CC { + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result { + self.deref().msats_per_minor_unit(iso4217_code) + } + + fn tolerance_percent(&self) -> u8 { + self.deref().tolerance_percent() + } +} + +/// 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 { + Err(()) + } + + fn tolerance_percent(&self) -> u8 { + 0 + } +} 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..e9d7443b356 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,21 @@ 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 } => currency_conversion + .msats_per_minor_unit(iso4217_code) + .map_err(|_| Bolt12SemanticError::UnsupportedCurrency)? + .checked_mul(amount) + .ok_or(Bolt12SemanticError::InvalidAmount), + } + } +} + /// An ISO 4217 three-letter currency code (e.g., USD). /// /// Currency codes must be exactly 3 ASCII uppercase letters. From 93a8c57302dff08f0c6ee7382b48ec6a2890c8e7 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 6 Mar 2026 20:40:44 +0530 Subject: [PATCH 2/6] Parameterize OffersMessageFlow over CurrencyConversion Makes OffersMessageFlow generic over a CurrencyConversion implementation, propagating the parameter through to ChannelManager. Upcoming changes will introduce currency conversion support in BOLT 12 message handling, which requires access to conversion logic from both ChannelManager and OffersMessageFlow. By threading the conversion abstraction through OffersMessageFlow now, subsequent commits can use it directly without introducing temporary plumbing or refactoring the type hierarchy later. --- fuzz/src/full_stack.rs | 18 ++++ lightning-background-processor/src/lib.rs | 8 ++ lightning-block-sync/src/init.rs | 6 +- lightning/src/ln/channelmanager.rs | 119 +++++++++++++++------- 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 | 15 ++- lightning/src/util/test_utils.rs | 18 ++++ 9 files changed, 158 insertions(+), 47 deletions(-) diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 3b7c99ea0b6..24f3e8e1856 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,18 @@ impl MessageRouter for FuzzRouter { } } +struct FuzzCurrencyConversion; + +impl CurrencyConversion for FuzzCurrencyConversion { + fn msats_per_minor_unit(&self, _iso4217_code: CurrencyCode) -> Result { + Err(()) + } + + fn tolerance_percent(&self) -> u8 { + 0 + } +} + struct TestBroadcaster { txn_broadcasted: Mutex>, } @@ -239,6 +253,7 @@ type ChannelMan<'a> = ChannelManager< Arc, &'a FuzzRouter, &'a FuzzRouter, + &'a FuzzCurrencyConversion, Arc, >; type PeerMan<'a> = PeerManager< @@ -549,6 +564,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 +630,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 4d6e770c099..47e17cc0315 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 a870f8ca88c..f5afab7ad47 100644 --- a/lightning-block-sync/src/init.rs +++ b/lightning-block-sync/src/init.rs @@ -51,6 +51,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; @@ -72,6 +73,7 @@ where /// F: FeeEstimator, /// R: Router, /// MR: MessageRouter, +/// CC: CurrencyConversion, /// L: Logger, /// C: chain::Filter, /// P: chainmonitor::Persist, @@ -86,6 +88,7 @@ where /// fee_estimator: &F, /// router: &R, /// message_router: &MR, +/// currency_conversion: &CC, /// logger: &L, /// persister: &P, /// ) { @@ -106,11 +109,12 @@ where /// tx_broadcaster, /// router, /// message_router, +/// currency_conversion, /// logger, /// config, /// vec![&mut monitor], /// ); -/// <(BlockHash, ChannelManager<&ChainMonitor, &T, &ES, &NS, &SP, &F, &R, &MR, &L>)>::read( +/// <(BlockHash, 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 30eb7f85d71..2fcde3051a1 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; @@ -1894,6 +1897,7 @@ pub type SimpleArcChannelManager = ChannelManager< >, >, Arc>>, Arc, Arc>>, + Arc, Arc, >; @@ -1925,6 +1929,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, >; @@ -1951,6 +1956,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. @@ -1965,6 +1972,7 @@ pub trait AChannelManager { Self::FeeEstimator, Self::Router, Self::MessageRouter, + Self::CurrencyConversion, Self::Logger, >; } @@ -1978,8 +1986,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > AChannelManager for ChannelManager + > AChannelManager for ChannelManager { type Watch = M; type Broadcaster = T; @@ -1990,8 +1999,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 } } @@ -2072,6 +2082,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, @@ -2087,18 +2098,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 (block_hash, channel_manager) = -/// <(BlockHash, ChannelManager<_, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; +/// <(BlockHash, ChannelManager<_, _, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; /// /// // Update the ChannelManager and ChannelMonitors with the latest chain data /// // ... @@ -2745,6 +2756,7 @@ pub struct ChannelManager< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, > { config: RwLock, @@ -2755,9 +2767,9 @@ pub struct ChannelManager< router: R, #[cfg(test)] - pub(super) flow: OffersMessageFlow, + pub(super) flow: OffersMessageFlow, #[cfg(not(test))] - flow: OffersMessageFlow, + flow: OffersMessageFlow, #[cfg(any(test, feature = "_test_utils"))] pub(super) best_block: RwLock, @@ -3545,8 +3557,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. /// @@ -3567,9 +3580,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, @@ -3583,7 +3596,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, + currency_conversion, logger.clone(), ); ChannelManager { @@ -14462,8 +14476,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>); @@ -15369,8 +15384,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()) @@ -15756,8 +15772,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. /// @@ -15781,8 +15798,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) { { @@ -15832,8 +15850,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) { @@ -15995,8 +16014,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 @@ -16347,8 +16367,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 @@ -16907,8 +16928,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > OffersMessageHandler for ChannelManager + > OffersMessageHandler for ChannelManager { #[rustfmt::skip] fn handle_message( @@ -17115,8 +17137,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, @@ -17362,8 +17385,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger, - > DNSResolverMessageHandler for ChannelManager + > DNSResolverMessageHandler for ChannelManager { fn handle_dnssec_query( &self, _message: DNSSECQuery, _responder: Option, @@ -17420,8 +17444,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) @@ -17945,8 +17970,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> { @@ -18675,6 +18701,7 @@ pub struct ChannelManagerReadArgs< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, > { /// A cryptographically secure source of entropy. @@ -18713,6 +18740,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, @@ -18753,16 +18785,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, @@ -18773,6 +18807,7 @@ impl< tx_broadcaster, router, message_router, + currency_conversion, logger, config, channel_monitors: hash_map_from_iter( @@ -18830,15 +18865,16 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ReadableArgs> - for (BlockHash, Arc>) + > ReadableArgs> + for (BlockHash, 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 (blockhash, chan_manager) = - <(BlockHash, ChannelManager)>::read(reader, args)?; + <(BlockHash, ChannelManager)>::read(reader, args)?; Ok((blockhash, Arc::new(chan_manager))) } } @@ -18853,12 +18889,13 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ReadableArgs> - for (BlockHash, ChannelManager) + > ReadableArgs> + for (BlockHash, 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( @@ -18885,8 +18922,9 @@ impl< F: FeeEstimator, R: Router, MR: MessageRouter, + CC: CurrencyConversion, L: Logger + Clone, - > ChannelManager + > ChannelManager { /// Constructs a `ChannelManager` from deserialized data and runtime dependencies. /// @@ -18898,7 +18936,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<(BlockHash, Self), DecodeError> { let ChannelManagerData { chain_hash, @@ -20121,6 +20159,7 @@ impl< args.node_signer.get_receive_auth_key(), secp_ctx.clone(), args.message_router, + args.currency_conversion, args.logger.clone(), ) .with_async_payments_offers_cache(async_receive_offer_cache); @@ -21749,6 +21788,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, >; @@ -21787,6 +21827,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); @@ -21795,7 +21836,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); @@ -21805,7 +21846,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 80274d180b4..f5a2836bfbe 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, >; @@ -566,6 +568,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>, @@ -751,6 +754,7 @@ pub trait NodeHolder { ::FeeEstimator, ::Router, ::MessageRouter, + ::CurrencyConversion, ::Logger, >; fn chain_monitor(&self) -> Option<&test_utils::TestChainMonitor<'_>>; @@ -768,6 +772,7 @@ impl NodeHolder for &H { ::FeeEstimator, ::Router, ::MessageRouter, + ::CurrencyConversion, ::Logger, > { (*self).node() @@ -898,6 +903,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( @@ -917,6 +923,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, @@ -1352,6 +1359,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, @@ -4622,6 +4630,7 @@ where Arc::clone(&network_graph), &cfg.keys_manager, ), + currency_conversion: test_utils::TestCurrencyConversion, chain_monitor, keys_manager: &cfg.keys_manager, node_seed: seed, @@ -4723,6 +4732,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, >, > { @@ -4737,6 +4747,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, @@ -4767,6 +4778,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, >, >, @@ -4835,6 +4847,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 a3252475965..a3447999b95 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -4868,6 +4868,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, @@ -4875,6 +4876,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 8d9eac5c001..72df7a60186 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -427,7 +427,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut nodes_0_read = &nodes_0_serialized[..]; if let Err(msgs::DecodeError::DangerousValue) = - <(BlockHash, 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 { + <(BlockHash, 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, @@ -435,6 +435,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, @@ -446,7 +447,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut nodes_0_read = &nodes_0_serialized[..]; let (_, nodes_0_deserialized_tmp) = - <(BlockHash, 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 { + <(BlockHash, 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, @@ -454,6 +455,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 6e7293cee6b..d904b45db2f 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::*; @@ -73,7 +74,7 @@ use { /// /// [`OffersMessageFlow`] is parameterized by a [`MessageRouter`], which is responsible /// for finding message paths when initiating and retrying onion messages. -pub struct OffersMessageFlow { +pub struct OffersMessageFlow { chain_hash: ChainHash, best_block: RwLock, @@ -86,6 +87,8 @@ pub struct OffersMessageFlow { secp_ctx: Secp256k1, message_router: MR, + pub(crate) currency_conversion: CC, + #[cfg(not(any(test, feature = "_test_utils")))] pending_offers_messages: Mutex>, #[cfg(any(test, feature = "_test_utils"))] @@ -102,13 +105,13 @@ pub struct OffersMessageFlow { logger: L, } -impl OffersMessageFlow { +impl OffersMessageFlow { /// Creates a new [`OffersMessageFlow`] pub fn new( chain_hash: ChainHash, best_block: BestBlock, our_network_pubkey: PublicKey, current_timestamp: u32, inbound_payment_key: inbound_payment::ExpandedKey, receive_auth_key: ReceiveAuthKey, secp_ctx: Secp256k1, message_router: MR, - logger: L, + currency_conversion: CC, logger: L, ) -> Self { Self { chain_hash, @@ -123,6 +126,8 @@ impl OffersMessageFlow { secp_ctx, message_router, + currency_conversion, + pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), @@ -257,7 +262,7 @@ const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 2 pub(crate) const TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY; -impl OffersMessageFlow { +impl OffersMessageFlow { /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. /// @@ -450,7 +455,7 @@ pub enum HeldHtlcReplyPath { }, } -impl OffersMessageFlow { +impl OffersMessageFlow { /// Verifies an [`InvoiceRequest`] using the provided [`OffersContext`] or the [`InvoiceRequest::metadata`]. /// /// - If an [`OffersContext::InvoiceRequest`] with a `nonce` is provided, verification is performed using recipient context data. diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index abcc24adf8d..da63ca34f1d 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -30,7 +30,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, }; @@ -447,6 +449,22 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { } } +pub struct TestCurrencyConversion; + +impl CurrencyConversion for TestCurrencyConversion { + fn msats_per_minor_unit(&self, iso4217_code: CurrencyCode) -> Result { + if iso4217_code.as_str() == "USD" { + Ok(1_000) // 1 cent = 1000 msats (test-only fixed rate) + } else { + Err(()) + } + } + + fn tolerance_percent(&self) -> u8 { + 0 + } +} + pub struct OnlyReadsKeysInterface {} impl EntropySource for OnlyReadsKeysInterface { From 8c268aef19eba68f09cebc12eb51b94d7edd05df Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 2 Mar 2026 16:12:58 +0530 Subject: [PATCH 3/6] Support setting fiat-denominated amounts in Offers Extends `OfferBuilder` to allow creating Offers whose amount is denominated in a fiat currency instead of millisatoshis. To ensure such Offers can later be processed correctly, currency amounts may only be set when the caller provides a `CurrencyConversion` implementation capable of resolving the amount into millisatoshis. Since amount correctness checks are now performed directly in the amount setters, they can be removed from the `build()` method. This introduces the first layer of currency support in Offers, allowing them to be created with currency-denominated amounts. --- lightning-dns-resolver/src/lib.rs | 2 +- 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 | 239 ++++++++---------- lightning/src/offers/static_invoice.rs | 45 ++-- lightning/src/offers/test_utils.rs | 3 +- 13 files changed, 318 insertions(+), 344 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index e9578844cf8..6d582685852 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -465,7 +465,7 @@ mod test { let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); - let bs_offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); + let bs_offer = nodes[1].node.create_offer_builder().unwrap().build(); let resolvers = vec![Destination::Node(resolver_id)]; pay_offer_flow( diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 25522346d9c..781a018832e 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) @@ -692,7 +692,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()) @@ -1672,7 +1672,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 2fcde3051a1..37d27892a80 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2469,8 +2469,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 b08b0f5a886..c76c4d451d7 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -3306,8 +3306,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() @@ -3355,8 +3355,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() @@ -3420,8 +3420,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() @@ -3509,8 +3509,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 d904b45db2f..9d1a36f293f 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1627,14 +1627,8 @@ impl OffersMessageFlow (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 e9d7443b356..0710a918f8c 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,31 @@ 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 > 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> + { + if amount.into_msats(currency_conversion)? > 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 +412,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 +423,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 +510,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 +707,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 +1006,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), ()> { @@ -1418,6 +1436,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; @@ -1426,7 +1445,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(); @@ -1477,30 +1496,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]); @@ -1509,7 +1522,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])); @@ -1518,8 +1531,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])); } @@ -1537,9 +1549,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)); @@ -1617,10 +1629,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)); @@ -1684,44 +1696,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), } @@ -1729,37 +1742,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); } @@ -1770,7 +1779,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)); @@ -1780,8 +1789,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)); @@ -1810,11 +1818,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))); @@ -1825,15 +1830,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"))); } @@ -1843,33 +1844,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)); @@ -1878,8 +1873,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); @@ -1897,7 +1891,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"), @@ -1907,11 +1900,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); } @@ -1919,10 +1909,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); } @@ -2054,7 +2045,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); } @@ -2062,8 +2053,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); } @@ -2104,8 +2095,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); } @@ -2120,8 +2110,7 @@ mod tests { ], )) .clear_issuer_signing_pubkey() - .build() - .unwrap(); + .build(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -2130,7 +2119,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) => { @@ -2144,30 +2133,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); } @@ -2175,7 +2160,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); } @@ -2204,7 +2189,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(); @@ -2220,7 +2205,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(); @@ -2236,7 +2221,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(); @@ -2249,7 +2234,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(); @@ -2265,7 +2250,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(); @@ -2278,7 +2263,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 24d14c0e295bf0b23b29fcb831fbe5742b905a25 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 28 Feb 2026 18:39:14 +0530 Subject: [PATCH 4/6] Introduce CurrencyConversion in InvoiceRequest builder To support currency-denominated Offers, the InvoiceRequest builder needs to resolve the Offer amount at multiple points during construction. This occurs when explicitly setting `amount_msats` and again when the InvoiceRequest is finalized via `build()`. To avoid repeatedly passing a `CurrencyConversion` implementation into these checks, the builder now stores a reference to it at creation time. This allows the builder to resolve currency-denominated Offer amounts whenever validation requires it. As part of this change, `InvoiceRequest::amount_msats()` is updated to use the provided `CurrencyConversion` to resolve the underlying Offer amount when necessary. --- fuzz/src/offer_deser.rs | 7 +- lightning/src/ln/async_payments_tests.rs | 4 + lightning/src/ln/channelmanager.rs | 24 +- lightning/src/ln/offers_tests.rs | 28 ++- lightning/src/ln/outbound_payment.rs | 14 +- lightning/src/offers/flow.rs | 13 +- lightning/src/offers/invoice.rs | 136 +++++++---- lightning/src/offers/invoice_request.rs | 291 ++++++++++++++--------- lightning/src/offers/merkle.rs | 15 +- lightning/src/offers/offer.rs | 97 +++++--- lightning/src/offers/refund.rs | 7 +- 11 files changed, 412 insertions(+), 224 deletions(-) diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs index 68902ab3150..ef37412254f 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)?; 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 781a018832e..f39b2a789e5 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; @@ -1448,6 +1449,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, @@ -1471,6 +1474,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 37d27892a80..f6574c048be 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8538,12 +8538,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 }, @@ -14679,10 +14693,12 @@ impl< 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), 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 c76c4d451d7..6b7c38b2059 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2932,7 +2932,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; @@ -3295,6 +3295,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!( @@ -3308,7 +3309,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() @@ -3352,12 +3353,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() @@ -3417,12 +3419,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() @@ -3507,11 +3510,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 9d1a36f293f..f8ecdb09d63 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -833,12 +833,19 @@ impl OffersMessageFlow( &'a self, offer: &'a Offer, nonce: Nonce, payment_id: PaymentId, - ) -> Result, Bolt12SemanticError> { + ) -> Result, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; + let conversion = &self.currency_conversion; + + let builder: InvoiceRequestBuilder<'a, 'a, secp256k1::All, CC> = + offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id, conversion)?.into(); + + let builder = match offer.resolve_offer_amount(conversion)? { + None => builder, + Some(amount_msats) => builder.amount_msats(amount_msats)?, + }; - let builder: InvoiceRequestBuilder = - offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id)?.into(); let builder = builder.chain_hash(self.chain_hash)?; Ok(builder) 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..d7f7a4e34bb 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)? @@ -71,15 +73,15 @@ 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::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) @@ -281,7 +287,7 @@ 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 + $self.currency_conversion, $self.invoice_request.amount_msats, $self.invoice_request.quantity )?; Ok($self.build_without_checks()) @@ -401,7 +407,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 +416,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 +717,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. + /// + /// 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). /// - /// [`chain`]: Self::chain - pub fn amount_msats(&$self) -> Option { - $contents.amount_msats() + /// [`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 +970,7 @@ impl InvoiceRequest { invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self, - InvoiceWithExplicitSigningPubkeyBuilder + InvoiceWithExplicitSigningPubkeyBuilder<'_> ); invoice_request_verify_method!(self, &Self); @@ -1084,7 +1105,7 @@ impl VerifiedInvoiceRequest { invoice_request_respond_with_derived_signing_pubkey_methods!( self, self.inner, - InvoiceWithDerivedSigningPubkeyBuilder + InvoiceWithDerivedSigningPubkeyBuilder<'_> ); } @@ -1103,7 +1124,7 @@ impl VerifiedInvoiceRequest { invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self.inner, - InvoiceWithExplicitSigningPubkeyBuilder + InvoiceWithExplicitSigningPubkeyBuilder<'_> ); } @@ -1143,17 +1164,23 @@ 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::UnsupportedCurrency)?; + + let quantity = self.quantity().unwrap_or(1); + + unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount) }, - }) + } } pub(super) fn has_amount_msats(&self) -> bool { @@ -1458,7 +1485,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 +1592,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 +1625,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 +1656,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 +1716,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 +1726,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 +1738,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 +1754,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 +1762,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 +1865,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 +1874,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 +1889,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 +1905,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 +1921,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 +1938,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 +1951,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 +1967,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 +1981,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 +1998,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 +2013,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 +2033,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 +2047,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 +2060,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 +2074,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 +2093,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 +2111,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 +2132,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 +2140,26 @@ 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_eq!(invoice_request.amount_msats(), None); + 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); } @@ -2124,12 +2170,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 +2189,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 +2207,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 +2217,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 +2230,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 +2245,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 +2254,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 +2262,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 +2277,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 +2286,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 +2294,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 +2307,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 +2323,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 +2342,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 +2360,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 +2385,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 +2411,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 +2435,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 +2459,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 +2479,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 +2495,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 +2514,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 +2537,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 +2558,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 +2583,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 +2593,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 +2610,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 +2635,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 +2656,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 +2679,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 +2700,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 +2720,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 +2743,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 +2776,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 +2807,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 +2842,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 +2872,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 +2905,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 +2916,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 +2951,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 +2986,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 +2997,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 +3035,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 +3070,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 +3098,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 +3142,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 +3161,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 0710a918f8c..5fc574e841e 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -798,20 +798,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)] { @@ -828,7 +831,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))] @@ -836,7 +839,7 @@ impl OfferFromHrn { request_invoice_derived_signing_pubkey!( self, self.offer, - InvoiceRequestBuilder<'a, 'b, T>, + InvoiceRequestBuilder<'a, 'b, T, CC>, Some(self.hrn) ); } @@ -846,7 +849,7 @@ impl Offer { request_invoice_derived_signing_pubkey!( self, self, - InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>, + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC>, None ); } @@ -856,7 +859,7 @@ impl OfferFromHrn { request_invoice_derived_signing_pubkey!( self, self.offer, - InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>, + InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b, CC>, Some(self.hrn) ); } @@ -943,28 +946,49 @@ 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); + + // Expected offer amount defaults to zero if unspecified + let expected_amount_msats = self + .resolve_offer_amount(currency_conversion)? + .map(|unit_msats| { + unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount) + }) + .transpose()?; + + 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) + }, - if amount_msats < expected_amount_msats { - return 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), - if amount_msats > MAX_VALUE_MSAT { - return Err(Bolt12SemanticError::InvalidAmount); - } + // 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(()) @@ -1544,6 +1568,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; @@ -1556,7 +1581,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(); @@ -1567,7 +1592,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(); @@ -1584,7 +1609,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(); @@ -1600,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(); @@ -1615,6 +1640,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), @@ -1637,7 +1663,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(); @@ -1648,7 +1674,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(); @@ -1663,7 +1689,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(); @@ -1681,7 +1707,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(); @@ -1887,11 +1913,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 8179a525a9acd6d308b6f12c3a0f0980ad17a87f Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 28 Feb 2026 19:39:56 +0530 Subject: [PATCH 5/6] Support currency-denominated Offers in InvoiceBuilder Adds currency conversion support when responding to an `InvoiceRequest` and constructing the `InvoiceBuilder`. When the underlying Offer specifies its amount in a currency denomination, the `CurrencyConversion` implementation is used to resolve the payable amount into millisatoshis and ensure the invoice amount satisfies the Offer's requirements. This reintroduces the currency validation intentionally skipped during `InvoiceRequest` parsing, keeping parsing focused on structural validation while enforcing amount correctness at the time the Invoice is constructed. --- lightning/src/ln/channelmanager.rs | 1 + lightning/src/ln/offers_tests.rs | 3 +- lightning/src/ln/outbound_payment.rs | 15 ++- lightning/src/offers/flow.rs | 27 +++-- lightning/src/offers/invoice.rs | 148 ++++++++++++++---------- lightning/src/offers/invoice_request.rs | 49 ++++---- 6 files changed, 142 insertions(+), 101 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f6574c048be..35d6b3416da 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5874,6 +5874,7 @@ impl< let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( invoice, + &self.flow.currency_conversion, payment_id, features, best_block_height, 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 6b7c38b2059..50f0111ef8b 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; @@ -1230,9 +1231,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 { @@ -1280,6 +1282,7 @@ impl OutboundPayments { let amount_msat = match InvoiceBuilder::::amount_msats( invreq, + currency_conversion, ) { Ok(amt) => amt, Err(_) => { @@ -3311,7 +3314,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(); @@ -3361,7 +3364,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(); @@ -3427,7 +3430,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 f8ecdb09d63..24b487353db 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}; @@ -866,10 +866,7 @@ impl OffersMessageFlow Some(amount_msats), - Amount::Currency { .. } => None, - }); + let amount_msat = offer.resolve_offer_amount(&self.currency_conversion)?; let created_at = self.duration_since_epoch(); @@ -998,9 +995,12 @@ impl OffersMessageFlow Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let conversion = &self.currency_conversion; - let amount_msats = - InvoiceBuilder::::amount_msats(&invoice_request.inner)?; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + conversion, + )?; let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; @@ -1021,9 +1021,10 @@ impl OffersMessageFlow OffersMessageFlow Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let conversion = &self.currency_conversion; - let amount_msats = - InvoiceBuilder::::amount_msats(&invoice_request.inner)?; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + conversion, + )?; let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; @@ -1080,9 +1084,10 @@ impl OffersMessageFlow 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,32 @@ 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 quantity = invoice_request.quantity().unwrap_or(1); + let requested_msats = invoice_request.amount_msats(currency_conversion)?; + + 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))] @@ -1932,7 +1949,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(); @@ -2204,7 +2221,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 +2236,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 +2318,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 +2426,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 +2447,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 +2480,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 +2511,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 +2531,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 +2560,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 +2617,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 +2646,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 +2664,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 +2692,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 +2770,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 +2815,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 +2849,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 +2894,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 +2937,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 +2981,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 +3047,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 +3136,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &conversion, payment_paths(), payment_hash(), now(), @@ -3142,6 +3167,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &conversion, payment_paths(), payment_hash(), now(), @@ -3184,7 +3210,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 +3239,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 +3304,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 +3338,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 +3382,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 +3421,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 +3468,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 +3494,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 +3535,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 +3573,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 +3615,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 +3651,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 +3700,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 +3747,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 d7f7a4e34bb..e7de125c0ca 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -787,14 +787,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. @@ -822,10 +823,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); } @@ -835,22 +837,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) } } } @@ -1019,14 +1022,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 @@ -1036,10 +1040,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); } @@ -1052,7 +1057,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 ) } } } @@ -1769,7 +1774,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() @@ -2371,7 +2376,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 8d617d039846c1459820d4d7367c413979bc7f8b Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 28 Feb 2026 21:01:07 +0530 Subject: [PATCH 6/6] Add tests for currency-denominated Offer flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests covering Offers whose amounts are denominated in fiat currencies. These tests verify that: * currency-denominated Offer amounts can be created * InvoiceRequests correctly resolve amounts using CurrencyConversion * Invoice construction validates and enforces the payable amount This ensures the full Offer → InvoiceRequest → Invoice flow works correctly when the original Offer amount is specified in currency. --- lightning/src/ln/offers_tests.rs | 73 ++++++++++++++++++++++++++++++++ lightning/src/offers/invoice.rs | 50 +++++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index f3243123732..d52086168c8 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}; @@ -916,6 +917,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.flow.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.flow.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. diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 41bc9724d37..d76b8be3746 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1894,7 +1894,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; @@ -2096,6 +2096,54 @@ mod tests { } } + #[test] + fn builds_invoice_for_fiat_offer() { + 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 currency_amount = + Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 }; + let expected_amount_msats = 10_000; + + let payment_paths = payment_paths(); + let payment_hash = payment_hash(); + let now = now(); + let unsigned_invoice = OfferBuilder::new(recipient_pubkey()) + .amount(currency_amount, &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.clone(), payment_hash, now) + .unwrap() + .build() + .unwrap(); + + let (_, offer_tlv_stream, _, invoice_tlv_stream, _, _, _) = + unsigned_invoice.contents.as_tlv_stream(); + assert_eq!(unsigned_invoice.amount(), Some(currency_amount)); + assert_eq!(offer_tlv_stream.currency, Some(b"USD")); + assert_eq!(offer_tlv_stream.amount, Some(10)); + assert_eq!(invoice_tlv_stream.amount, Some(expected_amount_msats)); + assert_eq!(unsigned_invoice.amount_msats(), expected_amount_msats); + assert_eq!(unsigned_invoice.payment_paths(), payment_paths.as_slice()); + + #[cfg(c_bindings)] + let mut unsigned_invoice = unsigned_invoice; + let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); + let (_, offer_tlv_stream, _, invoice_tlv_stream, _, _, _, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.amount(), Some(currency_amount)); + assert_eq!(offer_tlv_stream.currency, Some(b"USD")); + assert_eq!(offer_tlv_stream.amount, Some(10)); + assert_eq!(invoice_tlv_stream.amount, Some(expected_amount_msats)); + assert_eq!(invoice.amount_msats(), expected_amount_msats); + } + #[test] fn builds_invoice_for_refund_with_defaults() { let payment_paths = payment_paths();