-
Notifications
You must be signed in to change notification settings - Fork 416
Support Currency-Based Offers and Async Invoice Handling via FlowEvents #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
base: main
Are you sure you want to change the base?
Conversation
👋 I see @joostjager was un-assigned. |
cc @jkczyz |
🔔 1st Reminder Hey @joostjager! This PR has been waiting for your review. |
Is this proposed change a response to a request from a specific user/users? |
Hi @joostjager! This PR is actually a continuation of the original thread that led to the The motivation behind it was to provide users with the ability to handle So, as a first step, we worked on refactoring most of the Offers-related code out of 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! |
Another use case is Fedimint, where they'll want to include their own payment hash in the |
Does Fedimint plan to use the |
I believe with one. |
🔔 2nd Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 3rd Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 4th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 5th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 6th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 7th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 8th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 9th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 10th Reminder Hey @joostjager! This PR has been waiting for your review. |
🔔 11th Reminder Hey @joostjager! This PR has been waiting for your review. |
Removing @joostjager for now to stop bot spam. @shaavan and I have been working through some variations of this approach. |
lightning/src/offers/flow.rs
Outdated
amount_source, | ||
}; | ||
|
||
self.enqueue_offers_event(event)?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
lightning/src/offers/invoice.rs
Outdated
/// 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> { |
There was a problem hiding this comment.
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?
There was a problem hiding this 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
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Updated from pr3833.01 to pr3833.02 (diff): Changes:
|
/// to millisatoshis, handling any potential conversion errors. | ||
pub trait CurrencyConversion { | ||
/// Converts a fiat currency specified by its ISO 4217 code to millisatoshis. | ||
fn fiat_to_msats(&self, iso4217_code: CurrencyCode) -> Result<u64, Bolt12SemanticError>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make sure this is well documented. The conversion needs to the adjust for the ISO 4217 currency exponent. For example, a conversion from USD needs to be in terms of msats per cent (i.e., 1/100 of a dollar) not per dollar.
lightning/src/offers/invoice.rs
Outdated
@@ -1876,7 +1895,12 @@ mod tests { | |||
.unwrap() | |||
.build_and_sign() | |||
.unwrap() | |||
.respond_with_no_std(payment_paths.clone(), payment_hash, now) | |||
.respond_with_no_std( | |||
&DefaultCurrencyConversion {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we define an exchange_rate
test utility to avoid (most) line wrappings?
In the upcoming commits, we will be phasing the current style of VerifiedInvoiceRequest, in favour of newer version. To keep the changes modular, and clean we rename the current VerifiedInvoiceRequest to VerifiedInvoiceRequestLegacy.
In the following commits we will introduce `fields` function for other types as well, so to keep code DRY we convert the function to a macro.
This commit reintroduces `VerifiedInvoiceRequest`, now parameterized by `SigningPubkeyStrategy`. The key motivation is to restrict which functions can be called on a `VerifiedInvoiceRequest` based on its strategy type. This enables compile-time guarantees — ensuring that an incorrect `InvoiceBuilder` cannot be constructed for a given request, and misuses are caught early.
This commit replaces the legacy `VerifiedInvoiceRequestLegacy` with the new `InvoiceRequestVerifiedFromOffer` type in the codebase.
This change improves type safety and architectural clarity by introducing dedicated `InvoiceBuilder` methods tied to each variant of `VerifiedInvoiceRequestEnum`. With this change, users are now required to match on the enum variant before calling the corresponding builder method. This pushes the responsibility of selecting the correct builder to the user and ensures that invalid builder usage is caught at compile time, rather than relying on runtime checks. The signing logic has also been moved from the builder to the `ChannelManager`. This shift simplifies the builder's role and aligns it with the rest of the API, where builder methods return a configurable object that can be extended before signing. The result is a more consistent and predictable interface that separates concerns cleanly and makes future maintenance easier.
To ensure correct Bolt12 payment flow behavior, the `amount_msats` used for generating the `payment_hash`, `payment_secret`, and payment path must remain consistent. Previously, these steps could inadvertently diverge due to separate sources of `amount_msats`. This commit refactors the interface to use a `get_payment_info` closure, which captures the required variables and provides a single source of truth for both payment info (payment_hash, payment_secret) and path generation. This ensures consistency and eliminates subtle bugs that could arise from mismatched amounts across the flow.
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.
We introduce this check in pay_for_offer, to ensure that if the offer amount is specified in currency, a corresponding amount to be used in invoice request must be provided. **Reasoning:** When responding to an offer with currency, we enforce that the invoice request must always include an amount. This ensures we never receive an invoice tied to a currency-denominated offer without a corresponding request amount. By moving currency conversion upfront into the invoice request creation where the user can supply their own conversion logic — we avoid pushing conversion concerns into invoice parsing. This significantly reduces complexity during invoice verification.
Previously, the `enqueue_invoice` function in the `Flow` component accepted a `Refund` as input and dispatched the invoice either directly to a known `PublicKey` or via `BlindedMessagePath`s, depending on what was available within the `Refund`. While this worked for the refund-based flow, it tightly coupled invoice dispatch logic to the `Refund` abstraction, limiting its general usability outside of that context. The upcoming commits will introduce support for constructing and enqueuing invoices from manually handled `InvoiceRequest`s—decoupled from the `Refund` flow. To enable this, we are preemptively introducing more flexible, destination-specific variants of the enqueue function. Specifically, the `Flow` now exposes two dedicated methods: - `enqueue_invoice_using_node_id`: For sending an invoice directly to a known `PublicKey`. - `enqueue_invoice_using_reply_paths`: For sending an invoice over a set of explicitly provided `BlindedMessagePath`s. This separation improves clarity, enables reuse in broader contexts, and lays the groundwork for more composable invoice handling across the Offers/Refund flow.
Adds an API to send an `InvoiceError` to the counterparty via the flow. This becomes useful with the introduction of Flow events in upcoming commits, where the user can choose to either respond to Offers Messages or return an `InvoiceError`. Note: Given the small scope of changes in this commit, we also take the opportunity to perform minor documentation cleanups in `flow.rs`.
Until now, offers messages were processed internally without exposing intermediate steps. This made it harder for callers to intercept or analyse offer messages before deciding how to respond to them. `FlowEvents` provide an optional mechanism to surface these events back to the user. With events enabled, the caller can manually inspect an incoming message, choose to construct and sign an invoice, or send back an InvoiceError. This shifts control to the user where needed, while keeping the default automatic flow unchanged.
Updated from pr3833.02 to pr3833.03 (diff) Addressed @jkczyz comments Changes:
|
let (builder, _) = alice | ||
.node | ||
.flow | ||
.create_invoice_builder_from_invoice_request_with_keys( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be completely clear - is this the place where the strong types introduced in #3964 make a difference?
And to understand it better, if the keys
Option
would still have existed, and there would have been a single create_invoice_builder_from_invoice_request
method that switches based on whether keys are present that is called here, would anything go wrong?
Isn't a slightly different runtime check now needed to pattern match the FlowEvent btw (above), to get the stronger type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline, but summarizing here for completeness.
In main
, the flow looked like this:
ChannelManager
callsOffersMessageFlow
’s builder without checking whether keys are present.OffersMessageFlow
decides at runtime which method to call:respond_using_derived_keys
ifSome(key)
, orrespond_with
ifNone
.- The
respond_using...
methods then verify at runtime whether they were correctly called. Reference:
rust-lightning/lightning/src/offers/invoice_request.rs
Lines 948 to 951 in d16e65d
let keys = match $self.keys { None => return Err(Bolt12SemanticError::InvalidMetadata), Some(keys) => keys, };
This was fine in the native (LDK with ChannelManager) synchronous case, where OffersMessageFlow
is only used internally by ChannelManager
, and there's no risk of misuse from external callers.
However, this PR introduces support for FlowEvent
s and allows manual responses to InvoiceRequest
s using OffersMessageFlow
. That creates an entry point, for the native case where we now want compile-time guarantees that the correct builder is used with the correct key type.
So the updated flow becomes:
- At runtime: the caller (
ChannelManager
or a user) chooses whichOffersMessageFlow
builder to call. - At compile time: the builder enforces that the correct key-type combo is passed to
respond_using...
. - At compile time:
respond_using...
requires the correct type and no longer needs internal runtime checks.
This preserves behavior from main
for the synchronous ChannelManager
case, while providing stronger type safety for:
- Users manually creating
InvoiceRequest
responses viaFlowEvent
, and - Users building on top of
OffersMessageFlow
withoutChannelManager
(i.e., non-native LDK usage).
This PR addresses two current limitations in the LDK offer-handling flow:
InvoiceRequest
messages cannot be intercepted, inspected, or handled manually before responding.To solve this, we introduce a new
FlowEvents
interface that enables asynchronous handling ofInvoiceRequest
s, allowing developers to inspect, validate, or delay invoice generation as needed.We also parameterize the invoice-building flow with a
CurrencyConversion
trait, enabling developers to inject custom conversion logic and support offers denominated in fiat or other currencies. Developers can also supply a custom payment hash if needed.Together, these enhancements give developers greater control and flexibility over how invoices are constructed and how offers are processed—especially in use cases involving multiple currencies or asynchronous workflows.