@@ -172,6 +172,133 @@ impl AsyncReceiveOfferCache {
172172#[ cfg( async_payments) ]
173173const MAX_CACHED_OFFERS_TARGET : usize = 10 ;
174174
175+ // The max number of times we'll attempt to request offer paths per timer tick.
176+ #[ cfg( async_payments) ]
177+ const MAX_UPDATE_ATTEMPTS : u8 = 3 ;
178+
179+ // If we have an offer that is replaceable and its invoice was confirmed as persisted more than 2
180+ // hours ago, we can go ahead and refresh it because we always want to have the freshest offer
181+ // possible when a user goes to retrieve a cached offer.
182+ //
183+ // We avoid replacing unused offers too quickly -- this prevents the case where we send multiple
184+ // invoices from different offers competing for the same slot to the server, messages are received
185+ // delayed or out-of-order, and we end up providing an offer to the user that the server just
186+ // deleted and replaced.
187+ #[ cfg( async_payments) ]
188+ const OFFER_REFRESH_THRESHOLD : Duration = Duration :: from_secs ( 2 * 60 * 60 ) ;
189+
190+ #[ cfg( async_payments) ]
191+ impl AsyncReceiveOfferCache {
192+ /// Remove expired offers from the cache, returning whether new offers are needed.
193+ pub ( super ) fn prune_expired_offers (
194+ & mut self , duration_since_epoch : Duration , force_reset_request_attempts : bool ,
195+ ) -> bool {
196+ // Remove expired offers from the cache.
197+ let mut offer_was_removed = false ;
198+ for offer_opt in self . offers . iter_mut ( ) {
199+ let offer_is_expired = offer_opt
200+ . as_ref ( )
201+ . map_or ( false , |offer| offer. offer . is_expired_no_std ( duration_since_epoch) ) ;
202+ if offer_is_expired {
203+ offer_opt. take ( ) ;
204+ offer_was_removed = true ;
205+ }
206+ }
207+
208+ // Allow up to `MAX_UPDATE_ATTEMPTS` offer paths requests to be sent out roughly once per
209+ // minute, or if an offer was removed.
210+ if force_reset_request_attempts || offer_was_removed {
211+ self . reset_offer_paths_request_attempts ( )
212+ }
213+
214+ self . needs_new_offer_idx ( duration_since_epoch) . is_some ( )
215+ && self . offer_paths_request_attempts < MAX_UPDATE_ATTEMPTS
216+ }
217+
218+ /// If we have any empty slots in the cache or offers that can and should be replaced with a fresh
219+ /// offer, here we return the index of the slot that needs a new offer. The index is used for
220+ /// setting [`ServeStaticInvoice::invoice_slot`] when sending the corresponding new static invoice
221+ /// to the server, so the server knows which existing persisted invoice is being replaced, if any.
222+ ///
223+ /// Returns `None` if the cache is full and no offers can currently be replaced.
224+ ///
225+ /// [`ServeStaticInvoice::invoice_slot`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice_slot
226+ fn needs_new_offer_idx ( & self , duration_since_epoch : Duration ) -> Option < usize > {
227+ // If we have any empty offer slots, return the first one we find
228+ let empty_slot_idx_opt = self . offers . iter ( ) . position ( |offer_opt| offer_opt. is_none ( ) ) ;
229+ if empty_slot_idx_opt. is_some ( ) {
230+ return empty_slot_idx_opt;
231+ }
232+
233+ // If all of our offers are already used or pending, then none are available to be replaced
234+ let no_replaceable_offers = self
235+ . offers_with_idx ( )
236+ . all ( |( _, offer) | matches ! ( offer. status, OfferStatus :: Used | OfferStatus :: Pending ) ) ;
237+ if no_replaceable_offers {
238+ return None ;
239+ }
240+
241+ // All offers are pending except for one, so we shouldn't request an update of the only usable
242+ // offer
243+ let num_payable_offers = self
244+ . offers_with_idx ( )
245+ . filter ( |( _, offer) | {
246+ matches ! ( offer. status, OfferStatus :: Used | OfferStatus :: Ready { .. } )
247+ } )
248+ . count ( ) ;
249+ if num_payable_offers <= 1 {
250+ return None ;
251+ }
252+
253+ // Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they
254+ // were last updated, so they are stale enough to warrant replacement.
255+ let awhile_ago = duration_since_epoch. saturating_sub ( OFFER_REFRESH_THRESHOLD ) ;
256+ self . unused_offers ( )
257+ . filter ( |( _, _, invoice_confirmed_persisted_at) | {
258+ * invoice_confirmed_persisted_at < awhile_ago
259+ } )
260+ // Get the stalest offer and return its index
261+ . min_by ( |( _, _, persisted_at_a) , ( _, _, persisted_at_b) | {
262+ persisted_at_a. cmp ( & persisted_at_b)
263+ } )
264+ . map ( |( idx, _, _) | idx)
265+ }
266+
267+ /// Returns an iterator over (offer_idx, offer)
268+ fn offers_with_idx ( & self ) -> impl Iterator < Item = ( usize , & AsyncReceiveOffer ) > {
269+ self . offers . iter ( ) . enumerate ( ) . filter_map ( |( idx, offer_opt) | {
270+ if let Some ( offer) = offer_opt {
271+ Some ( ( idx, offer) )
272+ } else {
273+ None
274+ }
275+ } )
276+ }
277+
278+ /// Returns an iterator over (offer_idx, offer, invoice_confirmed_persisted_at)
279+ /// where all returned offers are [`OfferStatus::Ready`]
280+ fn unused_offers ( & self ) -> impl Iterator < Item = ( usize , & AsyncReceiveOffer , Duration ) > {
281+ self . offers_with_idx ( ) . filter_map ( |( idx, offer) | match offer. status {
282+ OfferStatus :: Ready { invoice_confirmed_persisted_at } => {
283+ Some ( ( idx, offer, invoice_confirmed_persisted_at) )
284+ } ,
285+ _ => None ,
286+ } )
287+ }
288+
289+ // Indicates that onion messages requesting new offer paths have been sent to the static invoice
290+ // server. Calling this method allows the cache to self-limit how many requests are sent.
291+ pub ( super ) fn new_offers_requested ( & mut self ) {
292+ self . offer_paths_request_attempts += 1 ;
293+ }
294+
295+ /// Called on timer tick (roughly once per minute) to allow another [`MAX_UPDATE_ATTEMPTS`] offer
296+ /// paths requests to go out.
297+ fn reset_offer_paths_request_attempts ( & mut self ) {
298+ self . offer_paths_request_attempts = 0 ;
299+ }
300+ }
301+
175302impl Writeable for AsyncReceiveOfferCache {
176303 fn write < W : Writer > ( & self , w : & mut W ) -> Result < ( ) , io:: Error > {
177304 write_tlv_fields ! ( w, {
0 commit comments