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

@shaavan shaavan commented Jun 7, 2025

Copy link
Copy Markdown
Member

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

ldk-reviews-bot commented Jun 7, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @jkczyz 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

shaavan commented Jun 7, 2025

Copy link
Copy Markdown
Member Author

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

shaavan commented Jun 11, 2025

Copy link
Copy Markdown
Member Author

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

jkczyz commented Jun 11, 2025

Copy link
Copy Markdown
Contributor

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

jkczyz commented Jun 11, 2025

Copy link
Copy Markdown
Contributor

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

jkczyz commented Jul 2, 2025

Copy link
Copy Markdown
Contributor

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

@vincenzopalazzo vincenzopalazzo left a comment

Copy link
Copy Markdown
Contributor

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

codecov Bot commented Aug 2, 2025

Copy link
Copy Markdown

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.

@shaavan

shaavan commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

Updated .22 → .23

Thanks, @jkczyz - Changes:

  • Introduce ExchangeRange::from_tolerance()
  • Fixed a test failure, due to limited checks.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd 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.

@shaavan shaavan force-pushed the currency branch 2 times, most recently from 1eb4160 to bd2a110 Compare June 3, 2026 11:19
@shaavan

shaavan commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Updated .23 → .24

Thanks, @jkczyz - Changes:

  • Introduce ExchangeRateBound. Make ExchangeRange internal struct.

@shaavan shaavan requested a review from jkczyz June 3, 2026 11:37
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

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

🔔 1st Reminder

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

🔔 2nd Reminder

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

Comment thread lightning/src/offers/currency.rs Outdated
Comment thread lightning/src/offers/currency.rs Outdated
Comment thread lightning/src/offers/currency.rs
Comment thread lightning/src/offers/offer.rs Outdated
Comment thread lightning/src/offers/offer.rs
Comment thread lightning/src/offers/currency.rs Outdated
Comment thread lightning/src/offers/currency.rs
Comment thread lightning/src/offers/currency.rs
Comment thread lightning/src/offers/currency.rs Outdated
shaavan and others added 11 commits June 9, 2026 21:24
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>
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

shaavan commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Updated .24 → .25

Thanks, @jkczyz - Changes:

  • Include BasisPoints representation for Tolerance, instead of Percentage.
  • Allow >100% upper tolerance.
  • Documented that minor unit value other than 1 should only be used in exceptional cases, and introduce appropriate constructors.

Comment on lines 1012 to +1037
@@ -1000,9 +1034,10 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
.map_err(|_| Bolt12SemanticError::MissingPaths)?;

#[cfg(all(feature = "std", not(fuzzing)))]
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
let builder = invoice_request.respond_using_derived_keys(converter, payment_paths, payment_hash);

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.

The currency amount is resolved twice from converter here: once at line 1012 (InvoiceBuilder::amount_msats), whose result is embedded into the blinded payment paths at line 1029, and again inside respond_using_derived_keysInvoiceBuilder::for_offerSelf::amount_msats(invoice_request, converter) at line 1037, which sets the actual invoice amount_msats.

For a CurrencyConversion that returns time-varying exchange rates (the intended fiat use case), the two lookups can return different values. The amount baked into the blinded payment path constraints (line 1029) would then disagree with the amount the payer is quoted in the invoice, which can break routing/claim consistency. The same pattern exists in create_invoice_builder_from_invoice_request_without_keys (lines 1081/1106).

Consider resolving the amount once and threading the resolved value into the invoice builder, rather than re-deriving it inside respond_with/respond_using_derived_keys.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th 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

🔔 7th 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.

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