Skip to content

Commit b7b8390

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 15f9f64 commit b7b8390

File tree

3 files changed

+63
-1
lines changed

3 files changed

+63
-1
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10749,9 +10749,23 @@ 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+
self.flow.get_async_receive_offer()
10760+
}
10761+
1075210762
/// Create an offer for receiving async payments as an often-offline recipient.
1075310763
///
10754-
/// Because we may be offline when the payer attempts to request an invoice, you MUST:
10764+
/// Instead of using this method, it is preferable to call
10765+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
10766+
/// [`Self::get_async_receive_offer`].
10767+
///
10768+
/// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST:
1075510769
/// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will
1075610770
/// serve the [`StaticInvoice`] created from this offer on our behalf.
1075710771
/// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this
@@ -10768,6 +10782,10 @@ where
1076810782
/// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were
1076910783
/// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the
1077010784
/// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`].
10785+
///
10786+
/// Instead of using this method to manually build the invoice, it is preferable to set
10787+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
10788+
/// [`Self::get_async_receive_offer`].
1077110789
#[cfg(async_payments)]
1077210790
pub fn create_static_invoice_builder<'a>(
1077310791
&self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option<Duration>,

lightning/src/offers/async_receive_offer_cache.rs

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

156156
#[cfg(async_payments)]
157157
impl AsyncReceiveOfferCache {
158+
/// Retrieve a cached [`Offer`] for receiving async payments as an often-offline recipient.
159+
pub fn get_async_receive_offer(&mut self, duration_since_epoch: Duration) -> Result<Offer, ()> {
160+
self.prune_expired_offers(duration_since_epoch, false);
161+
162+
// Find the freshest unused offer based on when the invoice was confirmed persisted by the
163+
// server
164+
let newest_ready_offer_opt = {
165+
self.offers_with_idx()
166+
.filter_map(|(idx, offer)| match offer.status {
167+
OfferStatus::Ready { invoice_confirmed_persisted_at } => {
168+
Some((idx, offer, invoice_confirmed_persisted_at))
169+
},
170+
_ => None,
171+
})
172+
.max_by(|a, b| a.2.cmp(&b.2))
173+
.map(|(idx, offer, _)| (idx, offer.offer.clone()))
174+
};
175+
if let Some((idx, newest_ready_offer)) = newest_ready_offer_opt {
176+
self.offers[idx].as_mut().map(|offer| offer.status = OfferStatus::Used);
177+
return Ok(newest_ready_offer);
178+
}
179+
180+
self.offers_with_idx()
181+
.filter(|(_, offer)| matches!(offer.status, OfferStatus::Used))
182+
.max_by(|a, b| {
183+
let abs_expiry_a = a.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
184+
let abs_expiry_b = b.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
185+
abs_expiry_a.cmp(&abs_expiry_b)
186+
})
187+
.map(|(_, cache_offer)| cache_offer.offer.clone())
188+
.ok_or(())
189+
}
190+
158191
/// Remove expired offers from the cache, returning whether new offers are needed.
159192
pub(super) fn prune_expired_offers(
160193
&mut self, duration_since_epoch: Duration, timer_tick_occurred: bool,

lightning/src/offers/flow.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,17 @@ where
11271127
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
11281128
}
11291129

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

0 commit comments

Comments
 (0)