-
Notifications
You must be signed in to change notification settings - Fork 418
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:
|
lightning/src/offers/invoice.rs
Outdated
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) |
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.
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.
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.
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 also chain all calls using and_then
where needed.
lightning/src/ln/channelmanager.rs
Outdated
@@ -2502,6 +2518,7 @@ pub struct ChannelManager< | |||
F::Target: FeeEstimator, | |||
R::Target: Router, | |||
MR::Target: MessageRouter, | |||
CC::Target: CurrencyConversion, |
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.
I'm not quite remembering our conversations, but why do we need to add a type parameter to ChannelManager
? I thought we could have methods on OffersMessageFlow
that take a CurrencyConversion
and have ChannelManager
pass it DefaultCurrencyConversion
. Then if a user wants currency conversion, they can request events and pass in their own CurrencyConversion
.
lightning/src/offers/flow.rs
Outdated
where | ||
MR::Target: MessageRouter, | ||
CC::Target: CurrencyConversion, |
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.
Likewise here. Though I seem to recall chatting about an option where OffersMessageFlow
is parameterized by a CurrencyConversion
like you have it, but it also would have methods that can be passed a CurrencyConversion
for use in the asynchronous event model. In that world, ChannelManager
would not be parameterized and would instead hardcode its OffersMessageFlow
with DefaultCurrencyConversion
.
lightning/src/offers/invoice.rs
Outdated
created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey, | ||
) -> Result<Self, Bolt12SemanticError> { | ||
let amount_msats = Self::amount_msats(invoice_request)?; | ||
pub(super) fn for_offer<CC: core::ops::Deref>( |
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 import Deref
.
pub fn respond_with( | ||
&$self, payment_paths: Vec<BlindedPaymentPath>, payment_hash: PaymentHash | ||
) -> Result<$builder, Bolt12SemanticError> { | ||
pub fn respond_with<CC: core::ops::Deref>( |
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 import Deref
here, too.
lightning/src/offers/invoice.rs
Outdated
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) |
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 also chain all calls using and_then
where needed.
/// A trait for converting fiat currencies into millisatoshi values. | ||
/// | ||
/// This is used for handling conversions between fiat currencies and Bitcoin denominated in millisatoshis | ||
/// when working with Bolt12 invoice requests. | ||
/// | ||
/// Implementors must provide a method to convert from a specified fiat currency (using ISO 4217 currency codes) | ||
/// to millisatoshis, handling any potential conversion errors. |
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.
Please wrap all documentation at 100 characters.
/// 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:
|
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.