Skip to content

Commit 069c6b8

Browse files
Add API to retrieve a cached async receive offer
Over the past multiple commits we've implemented interactively building async receive offers with a static invoice server that will service invoice requests on our behalf as an async recipient. Here we add an API to retrieve a resulting offer so we can receive payments when we're offline.
1 parent 4ec0bb9 commit 069c6b8

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10749,9 +10749,29 @@ where
1074910749
#[cfg(c_bindings)]
1075010750
create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder);
1075110751

10752+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only
10753+
/// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in
10754+
/// interactively building a [`StaticInvoice`] with the static invoice server.
10755+
///
10756+
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
10757+
#[cfg(async_payments)]
10758+
pub fn get_async_receive_offer(&self) -> Result<Offer, ()> {
10759+
let (offer, needs_persist) = self.flow.get_async_receive_offer()?;
10760+
if needs_persist {
10761+
// We need to re-persist the cache if a fresh offer was just marked as used to ensure we
10762+
// continue to keep this offer's invoice updated and don't replace it with the server.
10763+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
10764+
}
10765+
Ok(offer)
10766+
}
10767+
1075210768
/// Create an offer for receiving async payments as an often-offline recipient.
1075310769
///
10754-
/// Because we may be offline when the payer attempts to request an invoice, you MUST:
10770+
/// Instead of using this method, it is preferable to call
10771+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
10772+
/// [`Self::get_async_receive_offer`].
10773+
///
10774+
/// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST:
1075510775
/// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will
1075610776
/// serve the [`StaticInvoice`] created from this offer on our behalf.
1075710777
/// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this
@@ -10768,6 +10788,10 @@ where
1076810788
/// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were
1076910789
/// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the
1077010790
/// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`].
10791+
///
10792+
/// Instead of using this method to manually build the invoice, it is preferable to set
10793+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
10794+
/// [`Self::get_async_receive_offer`].
1077110795
#[cfg(async_payments)]
1077210796
pub fn create_static_invoice_builder<'a>(
1077310797
&self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option<Duration>,

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,45 @@ const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 30 * 24 * 60 * 60;
192192

193193
#[cfg(async_payments)]
194194
impl AsyncReceiveOfferCache {
195+
/// Retrieve a cached [`Offer`] for receiving async payments as an often-offline recipient, as
196+
/// well as returning a bool indicating whether the cache needs to be re-persisted.
197+
///
198+
// We need to re-persist the cache if a fresh offer was just marked as used to ensure we continue
199+
// to keep this offer's invoice updated and don't replace it with the server.
200+
pub fn get_async_receive_offer(
201+
&mut self, duration_since_epoch: Duration,
202+
) -> Result<(Offer, bool), ()> {
203+
self.prune_expired_offers(duration_since_epoch, false);
204+
205+
// Find the freshest unused offer, where "freshness" is based on when the invoice was confirmed
206+
// persisted by the server
207+
let newest_ready_offer_opt = self
208+
.offers_with_idx()
209+
.filter_map(|(idx, offer)| match offer.status {
210+
OfferStatus::Ready { invoice_confirmed_persisted_at } => {
211+
Some((idx, offer, invoice_confirmed_persisted_at))
212+
},
213+
_ => None,
214+
})
215+
.max_by(|a, b| a.2.cmp(&b.2))
216+
.map(|(idx, offer, _)| (idx, offer.offer.clone()));
217+
if let Some((idx, newest_ready_offer)) = newest_ready_offer_opt {
218+
self.offers[idx].as_mut().map(|offer| offer.status = OfferStatus::Used);
219+
return Ok((newest_ready_offer, true));
220+
}
221+
222+
// If no unused offers are available, return the used offer with the latest absolute expiry
223+
self.offers_with_idx()
224+
.filter(|(_, offer)| matches!(offer.status, OfferStatus::Used))
225+
.max_by(|a, b| {
226+
let abs_expiry_a = a.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
227+
let abs_expiry_b = b.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
228+
abs_expiry_a.cmp(&abs_expiry_b)
229+
})
230+
.map(|(_, cache_offer)| (cache_offer.offer.clone(), false))
231+
.ok_or(())
232+
}
233+
195234
/// Remove expired offers from the cache, returning whether new offers are needed.
196235
pub(super) fn prune_expired_offers(
197236
&mut self, duration_since_epoch: Duration, timer_tick_occurred: bool,

lightning/src/offers/flow.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,15 @@ where
11311131
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
11321132
}
11331133

1134+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only
1135+
/// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in
1136+
/// interactively building a [`StaticInvoice`] with the static invoice server.
1137+
#[cfg(async_payments)]
1138+
pub(crate) fn get_async_receive_offer(&self) -> Result<(Offer, bool), ()> {
1139+
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1140+
cache.get_async_receive_offer(self.duration_since_epoch())
1141+
}
1142+
11341143
/// Sends out [`OfferPathsRequest`] and [`ServeStaticInvoice`] onion messages if we are an
11351144
/// often-offline recipient and are configured to interactively build offers and static invoices
11361145
/// with a static invoice server.

0 commit comments

Comments
 (0)