Skip to content

Commit ea3bf1e

Browse files
committed
[feat] Advance recurrence after successful payment
On successful completion of a payment that corresponds to a recurrence we introduce logic to update the details after validating the invoice parameters. Co-authored-by: GLaDOS
1 parent 62b7fdc commit ea3bf1e

1 file changed

Lines changed: 90 additions & 1 deletion

File tree

‎src/event.rs‎

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use lightning::events::{
2424
};
2525
use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures};
2626
use lightning::ln::types::ChannelId;
27+
use lightning::offers::offer::Offer;
2728
use lightning::routing::gossip::NodeId;
2829
use lightning::sign::EntropySource;
2930
use lightning::util::config::{ChannelConfigOverrides, ChannelConfigUpdate};
@@ -36,8 +37,9 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage};
3637

3738
use crate::config::{may_announce_channel, Config, PEER_RECONNECTION_INTERVAL};
3839
use crate::connection::ConnectionManager;
39-
use crate::data_store::DataStoreUpdateResult;
40+
use crate::data_store::{DataStoreUpdateResult, StorableObject};
4041
use crate::fee_estimator::ConfirmationTarget;
42+
use crate::ffi::Bolt12Invoice;
4143
#[cfg(feature = "uniffi")]
4244
use crate::ffi::PaidBolt12Invoice;
4345
use crate::io::{
@@ -48,6 +50,7 @@ use crate::liquidity::LiquiditySource;
4850
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
4951
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
5052
use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
53+
use crate::payment::recurrence::RecurrenceStatus;
5154
use crate::payment::store::{
5255
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
5356
};
@@ -694,6 +697,82 @@ where
694697
})
695698
}
696699

700+
/// Processes a successful payment event for an outbound recurrence.
701+
async fn advance_recurrence_on_payment_sent(
702+
&self, payment_id: PaymentId, invoice: &Bolt12Invoice,
703+
) -> Result<(), ReplayEvent> {
704+
let mut matches = self
705+
.recurrence_store
706+
.list_filter(|details| details.active_payment_id == Some(payment_id))
707+
.await
708+
.into_iter();
709+
710+
let Some(mut details) = matches.next() else {
711+
// Ordinary payments do not have recurrence state and need no extra handling.
712+
return Ok(());
713+
};
714+
715+
debug_assert!(
716+
matches.next().is_none(),
717+
"multiple recurrences have the same active payment ID?"
718+
);
719+
720+
if details.last_successful_payment_id == Some(payment_id) {
721+
// Event replay after a completed transition must be idempotent.
722+
return Ok(());
723+
}
724+
725+
let offer = Offer::try_from(details.original_offer.clone()).map_err(|_| ReplayEvent())?;
726+
727+
if invoice.offer_id() != Some(offer.id()) {
728+
debug_assert!(false, "invoice offer ID does not match recurrence offer ID");
729+
return Ok(());
730+
}
731+
732+
let Some(invoice_recurrence) = invoice.invoice_recurrence() else {
733+
debug_assert!(false, "Invoice that should be corresponding to recurrence according to our recurrence store doesn't?");
734+
return Ok(());
735+
};
736+
737+
if let Some(recurrence_basetime) = details.basetime {
738+
if recurrence_basetime != invoice_recurrence.recurrence_basetime() {
739+
debug_assert!(false, "Recurrence basetime info mismatch");
740+
return Ok(());
741+
}
742+
}
743+
744+
details.paid_count = details.paid_count.saturating_add(1);
745+
details.basetime.get_or_insert(invoice_recurrence.recurrence_basetime());
746+
details.opaque_state =
747+
invoice_recurrence.recurrence_next_state().map(|state| state.to_vec());
748+
details.last_successful_payment_id = Some(payment_id);
749+
details.active_payment_id = None;
750+
751+
let recurrence = offer.offer_recurrence().ok_or(ReplayEvent())?;
752+
let period_index = recurrence
753+
.period_index(details.paid_count as u32, details.initial_start)
754+
.map_err(|_| ReplayEvent())?;
755+
756+
if recurrence.recurrence_limit.map(|limit| period_index >= limit.0).unwrap_or(false) {
757+
details.status = RecurrenceStatus::Completed;
758+
}
759+
760+
match self.recurrence_store.update(details.to_update()).await {
761+
Ok(DataStoreUpdateResult::Updated | DataStoreUpdateResult::Unchanged) => Ok(()),
762+
Ok(DataStoreUpdateResult::NotFound) => {
763+
log_error!(
764+
self.logger,
765+
"Recurrence disappeared before its payment update was stored"
766+
);
767+
Err(ReplayEvent())
768+
},
769+
Err(e) => {
770+
log_error!(self.logger, "Failed to store updated recurrence: {}", e);
771+
Err(ReplayEvent())
772+
},
773+
}
774+
}
775+
697776
async fn resolve_inbound_payment_id(
698777
&self, event_payment_id: Option<PaymentId>, payment_hash: &PaymentHash,
699778
) -> Result<(PaymentId, Option<PaymentDetails>), ReplayEvent> {
@@ -1413,6 +1492,16 @@ where
14131492
..PaymentDetailsUpdate::new(payment_id)
14141493
};
14151494

1495+
if let Some(invoice) = bolt12_invoice
1496+
.as_ref()
1497+
.and_then(|paid_invoice| paid_invoice.bolt12_invoice())
1498+
{
1499+
if let Err(e) = self.advance_recurrence_on_payment_sent(payment_id, invoice.as_ref()).await {
1500+
log_error!(self.logger, "Failed to advance recurrence: replaying event");
1501+
return Err(e);
1502+
}
1503+
}
1504+
14161505
match self.payment_store.update(update).await {
14171506
Ok(_) => {},
14181507
Err(e) => {

0 commit comments

Comments
 (0)