@@ -172,6 +172,133 @@ impl AsyncReceiveOfferCache {
172
172
#[ cfg( async_payments) ]
173
173
const MAX_CACHED_OFFERS_TARGET : usize = 10 ;
174
174
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
+
175
302
impl Writeable for AsyncReceiveOfferCache {
176
303
fn write < W : Writer > ( & self , w : & mut W ) -> Result < ( ) , io:: Error > {
177
304
write_tlv_fields ! ( w, {
0 commit comments