Skip to content

Introduce Synchronous Currency Conversion Support in Offers #3833

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Jun 7, 2025

This PR introduces support for Offers that specify amounts in fiat currencies by adding a synchronous currency conversion mechanism to the Flow.

To enable this, it defines a new CurrencyConversion trait, allowing users to provide custom fiat-to-msat conversion logic. This trait is threaded upstream into both Flow and ChannelManager, enabling unified handling of amount creation and verification, regardless of whether the Offer amount is denominated in Bitcoin or fiat.

The approach is intentionally synchronous, focusing on minimal, clean, and maintainable changes. While an synchronous model (e.g., event-driven just-in-time conversion or injecting custom payment_hash) is also being explored, it adds considerable complexity. By introducing currency support in a synchronous form first, this PR cleanly isolates the core functionality from broader architectural changes.

The trait exposes a single fiat-to-msat conversion function, where the user supplies a unit multiplier (e.g., msats per USD). All other logic, such as scaling by quantity and internal amount construction, is handled internally. This design reduces ambiguity, ensures correctness, and keeps the integration lightweight for the user.

In essence, this PR lays the foundation for robust fiat-denominated Offer support in a way that is simple, extensible, and forward-compatible.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 7, 2025

👋 I see @joostjager was un-assigned.
If you'd like another reviewer assignemnt, please click here.

@shaavan
Copy link
Member Author

shaavan commented Jun 7, 2025

cc @jkczyz

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@joostjager
Copy link
Contributor

Is this proposed change a response to a request from a specific user/users?

@shaavan
Copy link
Member Author

shaavan commented Jun 11, 2025

Hi @joostjager!

This PR is actually a continuation of the original thread that led to the OffersMessageFlow: link to thread.

The motivation behind it was to provide users with the ability to handle InvoiceRequests asynchronously—just like we already allow for Bolt12Invoices. However, adding more events into the middle of the ChannelManager flow felt suboptimal.

So, as a first step, we worked on refactoring most of the Offers-related code out of ChannelManager into the new OffersMessageFlow (#3639). Now that the refactor is complete, this PR picks up the original goal again: to let users asynchronously handle both InvoiceRequests and Invoices. This not only gives them more flexibility in analyzing these Offer messages, but also opens the door for creating custom interfaces—for example, to support Offers in different currency denominations.

Hope that gives a clear picture of the intent behind this! Let me know if you have any thoughts or suggestions—would love to hear them. Thanks a lot!

@jkczyz
Copy link
Contributor

jkczyz commented Jun 11, 2025

Another use case is Fedimint, where they'll want to include their own payment hash in the Bolt12Invoice.

@valentinewallace
Copy link
Contributor

Another use case is Fedimint, where they'll want to include their own payment hash in the Bolt12Invoice.

Does Fedimint plan to use the OffersMessageFlow without a ChannelManager?

@jkczyz
Copy link
Contributor

jkczyz commented Jun 11, 2025

Does Fedimint plan to use the OffersMessageFlow without a ChannelManager?

I believe with one.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 5th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 6th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 7th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 8th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 9th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 10th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 11th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz removed the request for review from joostjager July 2, 2025 13:38
@jkczyz
Copy link
Contributor

jkczyz commented Jul 2, 2025

Removing @joostjager for now to stop bot spam. @shaavan and I have been working through some variations of this approach.

amount_source,
};

self.enqueue_offers_event(event)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one comment - is native async in the picture too for this? We've converted other parts of LDK to be native async.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Joost! In pr3833.02, I’ve scoped this PR down to focus solely on introducing synchronous currency conversion support.

We’re still working through some open questions around the asynchronous path, including whether we can or want to go fully native async, so I’ve left that out for now to keep the scope clean. I’ll keep you posted as the design direction evolves.

Appreciate the nudge, thanks again!

/// A variant of [`InvoiceBuilder`] that indicates how the signing public key was set.
///
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
pub enum InvoiceBuilderVariant<'a> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to explain also in the docs what explicit and derived mean from an LDK point of view?

Copy link
Contributor

@vincenzopalazzo vincenzopalazzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK for me

I was just looking around to sync with this Offer Flow

shaavan added 2 commits August 1, 2025 23:44
Adds the `CurrencyConversion` trait to allow users to define custom
logic for converting fiat amounts into millisatoshis (msat).

This abstraction lays the groundwork for supporting Offers denominated
in fiat currencies, where conversion is inherently context-dependent.
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.
@shaavan shaavan changed the title Introduce Event Model for Offers Flow Introduce Synchronous Currency Conversion Support in Offers Aug 2, 2025
Copy link

codecov bot commented Aug 2, 2025

Codecov Report

❌ Patch coverage is 93.81107% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.92%. Comparing base (61e5819) to head (e087b63).

Files with missing lines Patch % Lines
lightning/src/offers/invoice_request.rs 87.23% 6 Missing ⚠️
lightning/src/ln/channelmanager.rs 66.66% 5 Missing ⚠️
lightning/src/offers/invoice.rs 97.61% 4 Missing and 1 partial ⚠️
lightning/src/util/test_utils.rs 50.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3833      +/-   ##
==========================================
- Coverage   88.94%   88.92%   -0.03%     
==========================================
  Files         174      174              
  Lines      124201   124409     +208     
  Branches   124201   124409     +208     
==========================================
+ Hits       110472   110626     +154     
- Misses      11251    11300      +49     
- Partials     2478     2483       +5     
Flag Coverage Δ
fuzzing 22.62% <9.00%> (-0.02%) ⬇️
tests 88.74% <93.81%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Following the introduction of the `CurrencyConversion` trait, this
commit integrates it into the `Flow` and `ChannelManager` as a
required parameter.

This change enables users to supply their own fiat-to-msat conversion
logic when constructing the Flow or ChannelManager, making synchronised
currency handling fully customizable at the application level.
@shaavan
Copy link
Member Author

shaavan commented Aug 2, 2025

Updated from pr3833.01 to pr3833.02 (diff):

Changes:

  • Narrows the scope of the PR to introduce only the synchronous CurrencyConversion trait, isolating it from broader architectural changes. See updated description for rationale.
  • Removes the asynchronous event-model integration, which will be explored in a future PR.

Comment on lines +421 to +423
Some(Amount::Currency { iso4217_code, amount }) => {
let unit_msats = currency_conversion.fiat_to_msats(iso4217_code)? * amount;
unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiplication of currency_conversion.fiat_to_msats(iso4217_code) * amount could overflow before the subsequent checked_mul(quantity) catches it. Consider using checked_mul for both operations to ensure safe arithmetic:

let unit_msats = currency_conversion.fiat_to_msats(iso4217_code)
    .checked_mul(amount)
    .ok_or(Bolt12SemanticError::InvalidAmount)?;
unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount)

This guards against overflow in both multiplication steps.

Suggested change
Some(Amount::Currency { iso4217_code, amount }) => {
let unit_msats = currency_conversion.fiat_to_msats(iso4217_code)? * amount;
unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount)
Some(Amount::Currency { iso4217_code, amount }) => {
let unit_msats = currency_conversion.fiat_to_msats(iso4217_code)?
.checked_mul(amount).ok_or(Bolt12SemanticError::InvalidAmount)?;
unit_msats.checked_mul(quantity).ok_or(Bolt12SemanticError::InvalidAmount)

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants