Skip to content

Commit 17b7100

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 fb8a40a commit 17b7100

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
@@ -11028,9 +11028,29 @@ where
1102811028
#[cfg(c_bindings)]
1102911029
create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder);
1103011030

11031+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only
11032+
/// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in
11033+
/// interactively building a [`StaticInvoice`] with the static invoice server.
11034+
///
11035+
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
11036+
#[cfg(async_payments)]
11037+
pub fn get_async_receive_offer(&self) -> Result<Offer, ()> {
11038+
let (offer, needs_persist) = self.flow.get_async_receive_offer()?;
11039+
if needs_persist {
11040+
// We need to re-persist the cache if a fresh offer was just marked as used to ensure we
11041+
// continue to keep this offer's invoice updated and don't replace it with the server.
11042+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
11043+
}
11044+
Ok(offer)
11045+
}
11046+
1103111047
/// Create an offer for receiving async payments as an often-offline recipient.
1103211048
///
11033-
/// Because we may be offline when the payer attempts to request an invoice, you MUST:
11049+
/// Instead of using this method, it is preferable to call
11050+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
11051+
/// [`Self::get_async_receive_offer`].
11052+
///
11053+
/// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST:
1103411054
/// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will
1103511055
/// serve the [`StaticInvoice`] created from this offer on our behalf.
1103611056
/// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this
@@ -11047,6 +11067,10 @@ where
1104711067
/// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were
1104811068
/// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the
1104911069
/// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`].
11070+
///
11071+
/// Instead of using this method to manually build the invoice, it is preferable to set
11072+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
11073+
/// [`Self::get_async_receive_offer`].
1105011074
#[cfg(async_payments)]
1105111075
pub fn create_static_invoice_builder<'a>(
1105211076
&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
@@ -195,6 +195,45 @@ const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 30 * 24 * 60 * 60;
195195

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

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

0 commit comments

Comments
 (0)