Skip to content

Commit 2d7e835

Browse files
committed
Add async offer refresh readiness APIs
To enable suppoort for async payments via LSPS2 JIT channels, we expose explicit async receive-offer refresh and readiness waiting so integrators can sequence external setup before relying on a ready async offer, instead of polling timer ticks. Generated with AI assistance. Co-Authored-By: HAL 9000
1 parent b4d6055 commit 2d7e835

3 files changed

Lines changed: 62 additions & 25 deletions

File tree

lightning-liquidity/src/lsps2/router.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ impl<R: Router, ES: EntropySource> LSPS2BOLT12Router<R, ES> {
9494
fn registered_lsps2_params(
9595
&self, payment_context: &PaymentContext,
9696
) -> Vec<LSPS2Bolt12InvoiceParameters> {
97-
// We intentionally only match `Bolt12Offer` here and not `AsyncBolt12Offer`, as LSPS2
98-
// JIT channels are not applicable to async (always-online) BOLT12 offer flows.
97+
// LSPS2 paths are applicable both to normal offers and async offers that resolve via a
98+
// static invoice server. In both cases the intercept SCID lets the LSP intercept the HTLC
99+
// and open the JIT channel before forwarding the payment.
99100
match payment_context {
100-
PaymentContext::Bolt12Offer(_) => {},
101+
PaymentContext::Bolt12Offer(_) | PaymentContext::AsyncBolt12Offer(_) => {},
101102
_ => return Vec::new(),
102103
};
103104

lightning/src/ln/channelmanager.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5857,30 +5857,46 @@ impl<
58575857
)
58585858
}
58595859

5860-
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) {
5860+
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) -> Result<(), ()> {
58615861
let peers = self.get_peers_for_blinded_path();
58625862
let channels = self.list_usable_channels();
58635863
let router = &self.router;
5864-
let refresh_res = self.flow.check_refresh_async_receive_offer_cache(
5864+
self.flow.check_refresh_async_receive_offer_cache(
58655865
peers,
58665866
channels,
58675867
router,
58685868
timer_tick_occurred,
5869-
);
5870-
match refresh_res {
5871-
Err(()) => {
5872-
log_error!(
5873-
self.logger,
5874-
"Failed to create blinded paths when requesting async receive offer paths"
5875-
);
5876-
},
5877-
Ok(()) => {},
5878-
}
5869+
)
58795870
}
58805871

58815872
#[cfg(test)]
58825873
pub(crate) fn test_check_refresh_async_receive_offers(&self) {
5883-
self.check_refresh_async_receive_offer_cache(false);
5874+
self.check_refresh_async_receive_offer_cache(false).unwrap();
5875+
}
5876+
5877+
/// Requests fresh async receive offer paths from the configured static invoice server, if any.
5878+
pub fn refresh_async_receive_offers(&self) -> Result<(), ()> {
5879+
self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
5880+
log_error!(
5881+
self.logger,
5882+
"Failed to create blinded paths when requesting async receive offer paths"
5883+
);
5884+
})
5885+
}
5886+
5887+
/// Waits for an async receive offer to become ready after the interactive static-invoice
5888+
/// protocol completes.
5889+
#[cfg(feature = "std")]
5890+
pub fn await_async_receive_offer(&self, max_wait: Duration) -> Result<Offer, ()> {
5891+
if let Ok(offer) = self.get_async_receive_offer() {
5892+
return Ok(offer);
5893+
}
5894+
5895+
if !self.flow.wait_for_async_receive_offer_ready(max_wait) {
5896+
return Err(());
5897+
}
5898+
5899+
self.get_async_receive_offer()
58845900
}
58855901

58865902
/// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder`
@@ -9101,7 +9117,12 @@ impl<
91019117
self.pending_outbound_payments
91029118
.remove_stale_payments(duration_since_epoch, &self.pending_events);
91039119

9104-
self.check_refresh_async_receive_offer_cache(true);
9120+
let _ = self.check_refresh_async_receive_offer_cache(true).map_err(|()| {
9121+
log_error!(
9122+
self.logger,
9123+
"Failed to create blinded paths when requesting async receive offer paths"
9124+
);
9125+
});
91059126

91069127
if self.check_free_holding_cells() {
91079128
// While we try to ensure we clear holding cells immediately, its possible we miss
@@ -15807,7 +15828,12 @@ impl<
1580715828
// interactively building offers as soon as we can after startup. We can't start building offers
1580815829
// until we have some peer connection(s) to receive onion messages over, so as a minor optimization
1580915830
// refresh the cache when a peer connects.
15810-
self.check_refresh_async_receive_offer_cache(false);
15831+
let _ = self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
15832+
log_error!(
15833+
self.logger,
15834+
"Failed to create blinded paths when requesting async receive offer paths"
15835+
);
15836+
});
1581115837
res
1581215838
}
1581315839

lightning/src/offers/flow.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ use crate::sync::{Mutex, RwLock};
6161
use crate::types::payment::{PaymentHash, PaymentSecret};
6262
use crate::util::logger::Logger;
6363
use crate::util::ser::Writeable;
64+
#[cfg(feature = "std")]
65+
use crate::util::wakers::Notifier;
6466

6567
/// A BOLT12 offers code and flow utility provider, which facilitates
6668
/// BOLT12 builder generation and onion message handling.
@@ -87,6 +89,8 @@ pub struct OffersMessageFlow<MR: MessageRouter, L: Logger> {
8789

8890
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>,
8991
async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>,
92+
#[cfg(feature = "std")]
93+
async_receive_offer_ready_notifier: Notifier,
9094

9195
logger: L,
9296
}
@@ -116,6 +120,8 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
116120
pending_async_payments_messages: Mutex::new(Vec::new()),
117121

118122
async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()),
123+
#[cfg(feature = "std")]
124+
async_receive_offer_ready_notifier: Notifier::new(),
119125

120126
logger,
121127
}
@@ -1361,7 +1367,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
13611367
Some(idx) => idx,
13621368
None => return Ok(()),
13631369
};
1364-
13651370
// If we need new offers, send out offer paths request messages to the static invoice server.
13661371
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
13671372
path_absolute_expiry: duration_since_epoch
@@ -1475,7 +1480,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
14751480
},
14761481
_ => return None,
14771482
};
1478-
14791483
// Create the blinded paths that will be included in the async recipient's offer.
14801484
let (offer_paths, paths_expiry) = {
14811485
let path_absolute_expiry =
@@ -1537,7 +1541,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15371541
},
15381542
_ => return None,
15391543
};
1540-
15411544
{
15421545
// Only respond with `ServeStaticInvoice` if we actually need a new offer built.
15431546
let mut cache = self.async_receive_offer_cache.lock().unwrap();
@@ -1569,7 +1572,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15691572
return None;
15701573
},
15711574
};
1572-
15731575
let (invoice, forward_invoice_request_path) = match self.create_static_invoice_for_server(
15741576
&offer,
15751577
offer_nonce,
@@ -1583,7 +1585,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15831585
return None;
15841586
},
15851587
};
1586-
15871588
if let Err(()) = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
15881589
offer,
15891590
message.paths_absolute_expiry,
@@ -1595,7 +1596,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15951596
log_error!(self.logger, "Failed to cache pending offer");
15961597
return None;
15971598
}
1598-
15991599
let reply_path_context = {
16001600
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
16011601
offer_id,
@@ -1711,7 +1711,17 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
17111711
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
17121712
pub fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
17131713
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1714-
cache.static_invoice_persisted(context)
1714+
let updated = cache.static_invoice_persisted(context);
1715+
#[cfg(feature = "std")]
1716+
if updated {
1717+
self.async_receive_offer_ready_notifier.notify();
1718+
}
1719+
updated
1720+
}
1721+
1722+
#[cfg(feature = "std")]
1723+
pub(crate) fn wait_for_async_receive_offer_ready(&self, max_wait: Duration) -> bool {
1724+
self.async_receive_offer_ready_notifier.get_future().wait_timeout(max_wait)
17151725
}
17161726

17171727
/// Get the encoded [`AsyncReceiveOfferCache`] for persistence.

0 commit comments

Comments
 (0)