-
Notifications
You must be signed in to change notification settings - Fork 421
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
👋 Thanks for assigning @jkczyz as a reviewer! |
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. |
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.60% 88.77% +0.17%
==========================================
Files 180 180
Lines 134878 136847 +1969
Branches 134878 136847 +1969
==========================================
+ Hits 119511 121492 +1981
+ Misses 12608 12547 -61
- Partials 2759 2808 +49
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:
|
// In this case we must enforce the Offer's minimum (if any): | ||
// reject if the IR's amount is below the Offer-implied floor. | ||
if let Some(ir_msats) = inner.amount_msats() { | ||
if offer_msats_opt.map_or(false, |offer_msats| ir_msats < offer_msats) { |
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.
Shouldn't it be true
if the offer doesn't have an amount?
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.
This is actually a failure condition:
if offer_msats_opt.map_or(false, |offer_msats| ir_msats < offer_msats) {
return Err(Bolt12SemanticError::InsufficientAmount);
}
So if the offer doesn’t specify an amount, any ir_msats
is accepted, which matches the “donation” case. That’s why I treat this here as valid rather than failing.
Let me know if I’m missing something in this reasoning.
Removed some comments that were meant for #3964. |
if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() { | ||
if amount_msats.is_none() { | ||
return Err(Bolt12SemanticError::MissingAmount); | ||
} | ||
} |
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.
Logic error: The validation checks if amount_msats.is_none()
for currency offers, but this validation occurs before the invoice request is created. At this point, amount_msats
represents the user-provided amount parameter, not the final computed amount from the invoice request. Currency offers may be valid without an explicit amount if the offer itself specifies the amount. This will incorrectly reject valid currency-based offers.
if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() { | |
if amount_msats.is_none() { | |
return Err(Bolt12SemanticError::MissingAmount); | |
} | |
} | |
if let Some(Amount::Currency { iso4217_code: _, amount }) = offer.amount() { | |
if amount.is_none() && amount_msats.is_none() { | |
return Err(Bolt12SemanticError::MissingAmount); | |
} | |
} |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
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 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.05 to pr3833.06 (diff) Changes:
|
🔔 1st Reminder Hey @jkczyz! This PR has been waiting for your review. |
🔔 2nd Reminder Hey @jkczyz! This PR has been waiting for your review. |
🔔 3rd Reminder Hey @jkczyz! This PR has been waiting for your review. |
Builds on #3964
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.