Skip to content

Commit a2569d8

Browse files
committed
Integrate CurrencyConversion into Bolt12Invoice amount handling
This commit updates the Bolt12Invoice amount creation logic to utilize the `CurrencyConversion` trait, enabling more flexible and customizable handling of fiat-to-msat conversions. Reasoning The `CurrencyConversion` trait is passed upstream into the invoice's amount creation flow, where it is used to interpret the Offer’s currency amount (if present) into millisatoshis. This change establishes a unified mechanism for amount handling—regardless of whether the Offer’s amount is denominated in Bitcoin or fiat, or whether the InvoiceRequest specifies an amount or not.
1 parent 4732a4f commit a2569d8

File tree

8 files changed

+239
-115
lines changed

8 files changed

+239
-115
lines changed

fuzz/src/invoice_request_deser.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ use lightning::blinded_path::payment::{
1717
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA;
1818
use lightning::ln::inbound_payment::ExpandedKey;
1919
use lightning::offers::invoice::UnsignedBolt12Invoice;
20-
use lightning::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields};
20+
use lightning::offers::invoice_request::{
21+
CurrencyConversion, InvoiceRequest, InvoiceRequestFields,
22+
};
2123
use lightning::offers::nonce::Nonce;
22-
use lightning::offers::offer::OfferId;
24+
use lightning::offers::offer::{CurrencyCode, OfferId};
2325
use lightning::offers::parse::Bolt12SemanticError;
2426
use lightning::sign::EntropySource;
2527
use lightning::types::features::BlindedHopFeatures;
@@ -79,6 +81,14 @@ fn privkey(byte: u8) -> SecretKey {
7981
SecretKey::from_slice(&[byte; 32]).unwrap()
8082
}
8183

84+
struct FuzzCurrencyConversion;
85+
86+
impl CurrencyConversion for FuzzCurrencyConversion {
87+
fn fiat_to_msats(&self, _iso4217_code: CurrencyCode) -> Result<u64, Bolt12SemanticError> {
88+
unreachable!()
89+
}
90+
}
91+
8292
fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
8393
invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1<T>,
8494
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
@@ -145,7 +155,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
145155
.unwrap();
146156

147157
let payment_hash = PaymentHash([42; 32]);
148-
invoice_request.respond_with(vec![payment_path], payment_hash)?.build()
158+
invoice_request.respond_with(&FuzzCurrencyConversion, vec![payment_path], payment_hash)?.build()
149159
}
150160

151161
pub fn invoice_request_deser_test<Out: test_logger::Output>(data: &[u8], out: Out) {

lightning/src/ln/channelmanager.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
9595
use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow};
9696
use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
9797
use crate::offers::invoice_error::InvoiceError;
98-
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestVerifiedFromOffer};
98+
use crate::offers::invoice_request::{
99+
DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestVerifiedFromOffer,
100+
};
99101
use crate::offers::nonce::Nonce;
100102
use crate::offers::offer::{Offer, OfferFromHrn};
101103
use crate::offers::parse::Bolt12SemanticError;
@@ -5698,6 +5700,7 @@ where
56985700
let features = self.bolt12_invoice_features();
56995701
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
57005702
invoice,
5703+
&DefaultCurrencyConversion,
57015704
payment_id,
57025705
features,
57035706
best_block_height,
@@ -15273,6 +15276,7 @@ where
1527315276
let result = self.flow.create_invoice_builder_from_invoice_request_with_keys(
1527415277
&self.router,
1527515278
&*self.entropy_source,
15279+
&DefaultCurrencyConversion,
1527615280
&request,
1527715281
self.list_usable_channels(),
1527815282
get_payment_info,
@@ -15298,6 +15302,7 @@ where
1529815302
let result = self.flow.create_invoice_builder_from_invoice_request_without_keys(
1529915303
&self.router,
1530015304
&*self.entropy_source,
15305+
&DefaultCurrencyConversion,
1530115306
&request,
1530215307
self.list_usable_channels(),
1530315308
get_payment_info,

lightning/src/ln/offers_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnou
5757
use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS;
5858
use crate::offers::invoice::Bolt12Invoice;
5959
use crate::offers::invoice_error::InvoiceError;
60-
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
60+
use crate::offers::invoice_request::{DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
6161
use crate::offers::nonce::Nonce;
6262
use crate::offers::parse::Bolt12SemanticError;
6363
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH};
@@ -2331,7 +2331,7 @@ fn fails_paying_invoice_with_unknown_required_features() {
23312331

23322332
let invoice = match verified_invoice_request {
23332333
InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
2334-
request.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap()
2334+
request.respond_using_derived_keys_no_std(&DefaultCurrencyConversion, payment_paths, payment_hash, created_at).unwrap()
23352335
.features_unchecked(Bolt12InvoiceFeatures::unknown())
23362336
.build_and_sign(&secp_ctx).unwrap()
23372337
},

lightning/src/ln/outbound_payment.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::ln::channelmanager::{
2323
use crate::ln::onion_utils;
2424
use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
2525
use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder};
26+
use crate::offers::invoice_request::CurrencyConversion;
2627
use crate::offers::invoice_request::InvoiceRequest;
2728
use crate::offers::nonce::Nonce;
2829
use crate::offers::static_invoice::StaticInvoice;
@@ -1115,13 +1116,15 @@ where
11151116
Ok(())
11161117
}
11171118

1118-
pub(super) fn static_invoice_received<ES: Deref>(
1119-
&self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures,
1120-
best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES,
1119+
pub(super) fn static_invoice_received<ES: Deref, CC: Deref>(
1120+
&self, invoice: &StaticInvoice, currency_conversion: CC, payment_id: PaymentId,
1121+
features: Bolt12InvoiceFeatures, best_block_height: u32, duration_since_epoch: Duration,
1122+
entropy_source: ES,
11211123
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>,
11221124
) -> Result<(), Bolt12PaymentError>
11231125
where
11241126
ES::Target: EntropySource,
1127+
CC::Target: CurrencyConversion,
11251128
{
11261129
macro_rules! abandon_with_entry {
11271130
($payment: expr, $reason: expr) => {
@@ -1168,6 +1171,7 @@ where
11681171

11691172
let amount_msat = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(
11701173
invreq,
1174+
currency_conversion,
11711175
) {
11721176
Ok(amt) => amt,
11731177
Err(_) => {
@@ -3206,7 +3210,7 @@ mod tests {
32063210
.build().unwrap()
32073211
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
32083212
.build_and_sign().unwrap()
3209-
.respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap()
3213+
.respond_with_no_conversion(payment_paths(), payment_hash(), created_at).unwrap()
32103214
.build().unwrap()
32113215
.sign(recipient_sign).unwrap();
32123216

@@ -3253,7 +3257,7 @@ mod tests {
32533257
.build().unwrap()
32543258
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
32553259
.build_and_sign().unwrap()
3256-
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
3260+
.respond_with_no_conversion(payment_paths(), payment_hash(), now()).unwrap()
32573261
.build().unwrap()
32583262
.sign(recipient_sign).unwrap();
32593263

@@ -3316,7 +3320,7 @@ mod tests {
33163320
.build().unwrap()
33173321
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
33183322
.build_and_sign().unwrap()
3319-
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
3323+
.respond_with_no_conversion(payment_paths(), payment_hash(), now()).unwrap()
33203324
.build().unwrap()
33213325
.sign(recipient_sign).unwrap();
33223326

lightning/src/offers/flow.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ use crate::offers::invoice::{
4040
DEFAULT_RELATIVE_EXPIRY,
4141
};
4242
use crate::offers::invoice_request::{
43-
InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer, VerifiedInvoiceRequest,
43+
CurrencyConversion, InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer,
44+
VerifiedInvoiceRequest,
4445
};
4546
use crate::offers::nonce::Nonce;
4647
use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder};
@@ -970,22 +971,31 @@ where
970971
/// Returns a [`Bolt12SemanticError`] if:
971972
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
972973
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
973-
pub fn create_invoice_builder_from_invoice_request_with_keys<'a, ES: Deref, R: Deref, F>(
974-
&self, router: &R, entropy_source: ES,
974+
pub fn create_invoice_builder_from_invoice_request_with_keys<
975+
'a,
976+
ES: Deref,
977+
R: Deref,
978+
F,
979+
CC: Deref,
980+
>(
981+
&self, router: &R, entropy_source: ES, currency_conversion: CC,
975982
invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>,
976983
usable_channels: Vec<ChannelDetails>, get_payment_info: F,
977984
) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError>
978985
where
979986
ES::Target: EntropySource,
980-
981987
R::Target: Router,
988+
CC::Target: CurrencyConversion,
982989
F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>,
983990
{
984991
let entropy = &*entropy_source;
992+
let conversion = &*currency_conversion;
985993
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
986994

987-
let amount_msats =
988-
InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(&invoice_request.inner)?;
995+
let amount_msats = InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(
996+
&invoice_request.inner,
997+
conversion,
998+
)?;
989999

9901000
let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?;
9911001

@@ -1007,9 +1017,10 @@ where
10071017
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
10081018

10091019
#[cfg(feature = "std")]
1010-
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
1020+
let builder = invoice_request.respond_using_derived_keys(conversion, payment_paths, payment_hash);
10111021
#[cfg(not(feature = "std"))]
10121022
let builder = invoice_request.respond_using_derived_keys_no_std(
1023+
conversion,
10131024
payment_paths,
10141025
payment_hash,
10151026
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),
@@ -1035,21 +1046,31 @@ where
10351046
/// Returns a [`Bolt12SemanticError`] if:
10361047
/// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`].
10371048
/// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`].
1038-
pub fn create_invoice_builder_from_invoice_request_without_keys<'a, ES: Deref, R: Deref, F>(
1039-
&self, router: &R, entropy_source: ES,
1049+
pub fn create_invoice_builder_from_invoice_request_without_keys<
1050+
'a,
1051+
ES: Deref,
1052+
R: Deref,
1053+
F,
1054+
CC: Deref,
1055+
>(
1056+
&self, router: &R, entropy_source: ES, currency_conversion: CC,
10401057
invoice_request: &'a VerifiedInvoiceRequest<ExplicitSigningPubkey>,
10411058
usable_channels: Vec<ChannelDetails>, get_payment_info: F,
10421059
) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError>
10431060
where
10441061
ES::Target: EntropySource,
10451062
R::Target: Router,
1063+
CC::Target: CurrencyConversion,
10461064
F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>,
10471065
{
10481066
let entropy = &*entropy_source;
1067+
let conversion = &*currency_conversion;
10491068
let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32;
10501069

1051-
let amount_msats =
1052-
InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(&invoice_request.inner)?;
1070+
let amount_msats = InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(
1071+
&invoice_request.inner,
1072+
conversion,
1073+
)?;
10531074

10541075
let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?;
10551076

@@ -1071,9 +1092,10 @@ where
10711092
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
10721093

10731094
#[cfg(feature = "std")]
1074-
let builder = invoice_request.respond_with(payment_paths, payment_hash);
1095+
let builder = invoice_request.respond_with(conversion, payment_paths, payment_hash);
10751096
#[cfg(not(feature = "std"))]
10761097
let builder = invoice_request.respond_with_no_std(
1098+
conversion,
10771099
payment_paths,
10781100
payment_hash,
10791101
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64),

0 commit comments

Comments
 (0)