Skip to content

Add currency conversion support for BOLT 12 offers#3833

Open
shaavan wants to merge 11 commits into
lightningdevkit:mainfrom
shaavan:currency
Open

Add currency conversion support for BOLT 12 offers#3833
shaavan wants to merge 11 commits into
lightningdevkit:mainfrom
shaavan:currency

Conversation

@shaavan
Copy link
Copy Markdown
Member

@shaavan shaavan commented Jun 7, 2025

This PR adds support for currency-denominated Offers in LDK’s BOLT 12 offer-handling flow.

Previously, Offers could only specify their amount in millisatoshis. However, BOLT 12 allows Offers to be denominated in other currencies such as fiat. Supporting this requires converting those currency amounts into millisatoshis at runtime when validating payments and constructing invoices.

Because exchange rates are external, time-dependent, and application-specific, LDK cannot perform these conversions itself. Instead, this PR introduces a CurrencyConversion trait which allows applications to provide their own logic for resolving currency-denominated amounts into millisatoshis. LDK remains exchange-rate agnostic and simply invokes this trait whenever a currency amount must be resolved.

To make this conversion logic available throughout the BOLT 12 flow, OffersMessageFlow is parameterized over a CurrencyConversion implementation and the abstraction is threaded through the offer handling pipeline.

With this in place:

  • OfferBuilder can now create Offers whose amounts are denominated in currencies instead of millisatoshis

InvoiceRequest handling can resolve Offer amounts when validating requests

InvoiceBuilder enforces that the final invoice amount satisfies the Offer’s requirements after resolving any currency denomination

Currency validation is intentionally deferred until invoice construction when necessary, keeping earlier stages focused on structural validation while ensuring the final payable amount is correct.

Tests are added to cover the complete Offer → InvoiceRequest → Invoice flow when the original Offer amount is specified in a currency.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Jun 7, 2025

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@shaavan
Copy link
Copy Markdown
Member Author

shaavan commented Jun 7, 2025

cc @jkczyz

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 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
Copy Markdown
Contributor

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

@shaavan
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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?

Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/offers/invoice.rs Outdated
Comment thread lightning/src/offers/invoice_request.rs Outdated
Comment thread lightning/src/offers/flow.rs Outdated
@jkczyz
Copy link
Copy Markdown
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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown

🔔 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
Copy Markdown
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.

Comment thread lightning/src/offers/flow.rs Outdated
Comment thread lightning/src/offers/invoice.rs Outdated
Copy link
Copy Markdown
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 shaavan changed the title Introduce Event Model for Offers Flow Introduce Synchronous Currency Conversion Support in Offers Aug 2, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 2, 2025

Codecov Report

❌ Patch coverage is 90.32258% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.37%. Comparing base (6749bc6) to head (a4742bd).
⚠️ Report is 85 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/offers/flow.rs 79.01% 16 Missing and 1 partial ⚠️
lightning/src/offers/offer.rs 61.53% 5 Missing ⚠️
lightning/src/offers/invoice.rs 94.36% 3 Missing and 1 partial ⚠️
lightning/src/offers/invoice_request.rs 94.44% 3 Missing ⚠️
lightning/src/ln/channelmanager.rs 92.30% 1 Missing and 1 partial ⚠️
lightning/src/ln/offers_tests.rs 97.70% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3833      +/-   ##
==========================================
+ Coverage   89.34%   89.37%   +0.02%     
==========================================
  Files         180      180              
  Lines      138480   140045    +1565     
  Branches   138480   140045    +1565     
==========================================
+ Hits       123730   125164    +1434     
- Misses      12129    12295     +166     
+ Partials     2621     2586      -35     
Flag Coverage Δ
fuzzing 35.13% <4.10%> (-0.84%) ⬇️
tests 88.71% <90.32%> (+<0.01%) ⬆️

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.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 8th Reminder

Hey @TheBlueMatt! 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
Copy Markdown

🔔 9th Reminder

Hey @TheBlueMatt! 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
Copy Markdown

🔔 10th Reminder

Hey @TheBlueMatt! 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
Copy Markdown

🔔 11th Reminder

Hey @TheBlueMatt! 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
Copy Markdown

🔔 12th Reminder

Hey @TheBlueMatt! 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.

Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Any updates after @jkczyz's review above?

@shaavan shaavan force-pushed the currency branch 2 times, most recently from 6185f86 to d0ca659 Compare May 21, 2026 17:32
@shaavan
Copy link
Copy Markdown
Member Author

shaavan commented May 21, 2026

Updated .17 → .18

Thanks, @jkczyz - Changes:

  • Cleaned up commits and improved ordering. Plumbing changes are now separated from the feature commits for easier review.
  • Replaced tolerance_percent: u8 with ExchangeRange.
  • Introduced ExchangeRate, allowing exchange rates to express sub-msat-level precision for currency conversion (see the commits for details).

Reasoning:

I favoured returning an ExchangeRange over a tolerance_percent: u8 for a few reasons:

  • A percentage-based API introduces implicit constraints and assumptions (for example, needing additional validation around acceptable bounds), while ExchangeRange makes the accepted values explicit.
  • Returning a range gives implementers flexibility to define their own conversion policy while keeping the calculation opaque. Different currencies can naturally use different tolerances and methodologies.
  • Accepted exchange ranges do not need to be symmetric around a single reference rate. Representing the result directly as a range allows more flexible policies, for example (123 msats / 10_000 minor_units) to (123 msats / 1_000 minor_units).

See the ExchangeRate documentation introduced in the commit for details on how (X msats / Y minor_units) maps to practical conversion behaviour.

Comment thread lightning/src/offers/flow.rs
Comment thread lightning/src/offers/flow.rs
Comment thread lightning/src/ln/outbound_payment.rs
@shaavan
Copy link
Copy Markdown
Member Author

shaavan commented May 23, 2026

Updated .18 → .19

Thanks, @ldk-claude-review-bot - Changes:

  • Solve compile time error for non-std branch
  • Introduced correct error type for failure branch in static_invoice_received.

shaavan and others added 2 commits May 23, 2026 17:01
Add a `CurrencyConversion` trait for resolving currency-denominated amounts
into millisatoshis.

LDK cannot supply exchange rates itself, so applications provide this
conversion logic as the foundation for fiat-denominated offer support.
Thread `CurrencyConversion` through `ChannelManager` type parameters and
construction APIs.

BOLT12 offer amounts will require currency conversion during invoice
construction and payment validation. Wire the conversion dependency
through `ChannelManager` now so later commits can use a single conversion
path instead of duplicating conversion logic across call sites.

Wire the dependency through the related test, fuzz, and helper
scaffolding to support the new manager integration.

AI-assisted: Dependency plumbing and scaffolding.

Co-Authored-By: OpenAI Codex <codex@openai.com>
Comment thread lightning/src/ln/channelmanager.rs
shaavan and others added 9 commits May 23, 2026 17:20
BOLT12 currency-denominated offers will require currency conversion
during offer construction to validate the set amount.

This commit handles the plumbing needed to introduce that behavior,
threading `CurrencyConversion` through the relevant offer-building
paths and scaffolding. The next commit will introduce the actual
conversion logic.

Keep the plumbing and logical changes separate to make the transition
easier to review.
This commit completes the second part of currency conversion support for
offers by adding validation for currency-denominated amounts during
offer construction.

With the plumbing introduced in the previous commit now in place,
`OfferBuilder` can support currency-denominated offer amounts and expose
the related amount-setting APIs publicly.

Add minimal tests to verify that the public amount APIs work correctly
and that fiat-denominated offers build successfully.
For currency-denominated offers, the payer may not reliably derive the
final msat amount during invoice request creation. Instead, defer the
final amount resolution to invoice creation (for the payee) and
invoice handling (for the payer), where the currency conversion can
be verified against the offer's fiat amount.

This also updates a previously failing test to cover the new behavior.

Additional reasoning is documented in the commit.
The old `InvoiceRequest` amount accessor blurred two different concepts:
the amount explicitly carried in the request TLV and the amount derived
from the offer and quantity. Callers had to pair `amount_msats` with
`has_amount_msats` to determine whether the amount was actually present
in the request or synthesized on read.

Split those meanings into separate accessors:
- `amount_msats()` returns the amount explicitly requested in the
invoice request.
- `payable_amount_msats()` returns the payable amount for the invoice
request, deriving it from the offer when needed.

As part of the recent currency conversion support,
`payable_amount_msats()` now accepts a conversion trait parameter,
allowing callers to derive payable amounts even when the offer is
currency-denominated.

This lays the groundwork for future commits adding currency conversion
support to `Bolt12Invoice` creation and handling logic.
Thread CurrencyConversion through the InvoiceBuilder construction flow
and the related upstream APIs.

This sets up the plumbing needed for currency-denominated invoice
handling without introducing the actual verification logic yet.

The plumbing and logical changes are separated to make the transition
easier to review.

The next commit adds payer-side invoice amount verification, completing
the end-to-end currency conversion flow.
Currency-denominated offers may not include an explicit msat amount
in the invoice request. During invoice building, we now use the
configured currency conversion to either validate the requested amount
or derive the payable amount from the offer amount.

This completes the currency conversion support on the payee side.

The next commit adds payer-side invoice amount verification,
completing the end-to-end currency conversion flow.
Add tests covering invoice request and invoice response handling for
currency-denominated offers.

This combines coverage for the standard flow that derives the final
invoice amount through currency conversion and the insufficient-msat
request path that must be rejected while building the invoice
response.

The merged test coverage exercises both the positive and deferred-
validation paths for currency-denominated invoice responses.

AI-assisted: Planning and writing the tests

Co-Authored-By: OpenAI Codex <codex@openai.com>
This completes the invoice handling side of currency conversion support.

When paying an invoice for a currency-denominated offer, and the
invoice request did not specify an explicit amount, we now use the
configured CurrencyConversion to derive the acceptable msat range
for the offer amount.

The invoice is considered valid only if the quoted amount falls within
that acceptable range, preventing the payer from being overcharged due
to exchange-rate differences or unexpected invoice amounts.
Add end-to-end and payer-side tests for currency-denominated
offers and invoices.

This consolidates coverage for the standard payment flow,
excessive invoice rejection, unverifiable fiat invoices when
conversion support is unavailable, and quantity-scaled invoice
requests.

The combined coverage exercises the main invoice amount
verification paths introduced by currency-denominated offer
support.

AI-assisted: Planning and writing the tests

Co-Authored-By: OpenAI Codex <codex@openai.com>
@shaavan
Copy link
Copy Markdown
Member Author

shaavan commented May 23, 2026

Rebased .19 → .20

{
#[cfg(not(c_bindings))]
create_offer_builder!(self, OfferBuilder<'_, DerivedMetadata, secp256k1::All>);
create_offer_builder!(self, OfferBuilder<'_, DerivedMetadata, secp256k1::All, CC>);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug (won't compile under c_bindings): The non-c_bindings path was correctly updated to add CC here, but the c_bindings path at line 14896 (create_offer_builder!(self, OfferWithDerivedMetadataBuilder)) was NOT updated. Since OfferWithDerivedMetadataBuilder now requires a CC type parameter, the c_bindings invocation will fail to compile. It should be OfferWithDerivedMetadataBuilder<'_, CC>.


/// A range of accepted exchange rates.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExchangeRange {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing invariant enforcement: ExchangeRange has pub fields and no constructor, so callers can construct instances where minimum > maximum (i.e., the minimum rate converts to more msats than the maximum rate). Some callers only use minimum (e.g., InvoiceBuilder::amount_msats uses only the lower bound) and some only use maximum (e.g., verify_amount_acceptable_for_payment uses only the upper bound). If the range is inverted, these callers will silently use the wrong bound.

OfferBuilder::build() does catch minimum_msats > maximum_msats, but that's a builder-time check that only protects offer creation, not the invoice/verification paths.

Consider either:

  1. Adding a constructor that validates minimum <= maximum, or
  2. Having to_msats_range sort the values before returning

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.

8 participants