Skip to content

Commit bd8c650

Browse files
committed
Update EventHandler to create inbound PaymentStore entries on demand
With receive-side pre-creation removed, the event handler must now create `PaymentStore` entries when it first encounters an inbound payment. In the `PaymentClaimable` handler, when a `Bolt11InvoicePayment` is not found in the store: - Manual-claim path (`preimage == None`): check the metadata store for `LSPFeeLimits`, validate counterparty-skimmed fees, create a `Bolt11Jit` or `Bolt11` entry, and emit `PaymentClaimable`. - Auto-claim path (`preimage == Some`): same fee-limit check and entry creation, then fall through to `claim_funds`. In the `PaymentClaimed` handler, when the update returns `NotFound`, insert a new entry using the payment purpose to determine the kind. Outbound payment handlers (`PaymentSent`, `PaymentFailed`) are unchanged since entries are still pre-created by `send()` / `send_using_amount()`. Generated with the assistance of AI tools. Co-Authored-By: HAL 9000
1 parent 3adeeb0 commit bd8c650

File tree

2 files changed

+273
-30
lines changed

2 files changed

+273
-30
lines changed

src/event.rs

Lines changed: 264 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,184 @@ where
859859
amount_msat,
860860
);
861861
let payment_preimage = match purpose {
862-
PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => {
862+
PaymentPurpose::Bolt11InvoicePayment {
863+
payment_preimage,
864+
payment_secret,
865+
..
866+
} => {
867+
if payment_preimage.is_none() {
868+
// This is a manual-claim (`_for_hash`) payment that was not
869+
// pre-registered in the payment store. Check metadata store for
870+
// LSP fee limits and create the store entry.
871+
let lsp_fee_limits = self
872+
.payment_metadata_store
873+
.get_lsp_fee_limits_for_payment_id(&payment_id);
874+
875+
if let Some(ref limits) = lsp_fee_limits {
876+
let max_total_opening_fee_msat = limits
877+
.max_total_opening_fee_msat
878+
.or_else(|| {
879+
limits.max_proportional_opening_fee_ppm_msat.and_then(
880+
|max_prop_fee| {
881+
compute_opening_fee(amount_msat, 0, max_prop_fee)
882+
},
883+
)
884+
})
885+
.unwrap_or(0);
886+
887+
if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
888+
log_info!(
889+
self.logger,
890+
"Refusing inbound payment with hash {} as the counterparty-withheld fee of {}msat exceeds our limit of {}msat",
891+
hex_utils::to_string(&payment_hash.0),
892+
counterparty_skimmed_fee_msat,
893+
max_total_opening_fee_msat,
894+
);
895+
self.channel_manager.fail_htlc_backwards(&payment_hash);
896+
return Ok(());
897+
}
898+
}
899+
900+
let kind = if lsp_fee_limits.is_some() {
901+
PaymentKind::Bolt11Jit {
902+
hash: payment_hash,
903+
preimage: None,
904+
secret: Some(payment_secret),
905+
counterparty_skimmed_fee_msat: if counterparty_skimmed_fee_msat
906+
> 0
907+
{
908+
Some(counterparty_skimmed_fee_msat)
909+
} else {
910+
None
911+
},
912+
lsp_fee_limits: lsp_fee_limits.unwrap(),
913+
}
914+
} else {
915+
PaymentKind::Bolt11 {
916+
hash: payment_hash,
917+
preimage: None,
918+
secret: Some(payment_secret),
919+
}
920+
};
921+
922+
let payment = PaymentDetails::new(
923+
payment_id,
924+
kind,
925+
Some(amount_msat),
926+
None,
927+
PaymentDirection::Inbound,
928+
PaymentStatus::Pending,
929+
);
930+
931+
match self.payment_store.insert(payment) {
932+
Ok(_) => {},
933+
Err(e) => {
934+
log_error!(
935+
self.logger,
936+
"Failed to insert payment with ID {}: {}",
937+
payment_id,
938+
e
939+
);
940+
return Err(ReplayEvent());
941+
},
942+
}
943+
944+
let custom_records = onion_fields
945+
.map(|cf| {
946+
cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect()
947+
})
948+
.unwrap_or_default();
949+
let event = Event::PaymentClaimable {
950+
payment_id,
951+
payment_hash,
952+
claimable_amount_msat: amount_msat,
953+
claim_deadline,
954+
custom_records,
955+
};
956+
match self.event_queue.add_event(event).await {
957+
Ok(_) => return Ok(()),
958+
Err(e) => {
959+
log_error!(self.logger, "Failed to push to event queue: {}", e);
960+
return Err(ReplayEvent());
961+
},
962+
};
963+
} else {
964+
// Auto-claim path: payment has a preimage but was not
965+
// pre-registered in the store. Check metadata store for
966+
// LSP fee limits and create the store entry before claiming.
967+
let lsp_fee_limits = self
968+
.payment_metadata_store
969+
.get_lsp_fee_limits_for_payment_id(&payment_id);
970+
971+
if let Some(ref limits) = lsp_fee_limits {
972+
let max_total_opening_fee_msat = limits
973+
.max_total_opening_fee_msat
974+
.or_else(|| {
975+
limits.max_proportional_opening_fee_ppm_msat.and_then(
976+
|max_prop_fee| {
977+
compute_opening_fee(amount_msat, 0, max_prop_fee)
978+
},
979+
)
980+
})
981+
.unwrap_or(0);
982+
983+
if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
984+
log_info!(
985+
self.logger,
986+
"Refusing inbound payment with hash {} as the counterparty-withheld fee of {}msat exceeds our limit of {}msat",
987+
hex_utils::to_string(&payment_hash.0),
988+
counterparty_skimmed_fee_msat,
989+
max_total_opening_fee_msat,
990+
);
991+
self.channel_manager.fail_htlc_backwards(&payment_hash);
992+
return Ok(());
993+
}
994+
}
995+
996+
let kind = if lsp_fee_limits.is_some() {
997+
PaymentKind::Bolt11Jit {
998+
hash: payment_hash,
999+
preimage: payment_preimage,
1000+
secret: Some(payment_secret),
1001+
counterparty_skimmed_fee_msat: if counterparty_skimmed_fee_msat
1002+
> 0
1003+
{
1004+
Some(counterparty_skimmed_fee_msat)
1005+
} else {
1006+
None
1007+
},
1008+
lsp_fee_limits: lsp_fee_limits.unwrap(),
1009+
}
1010+
} else {
1011+
PaymentKind::Bolt11 {
1012+
hash: payment_hash,
1013+
preimage: payment_preimage,
1014+
secret: Some(payment_secret),
1015+
}
1016+
};
1017+
1018+
let payment = PaymentDetails::new(
1019+
payment_id,
1020+
kind,
1021+
Some(amount_msat),
1022+
None,
1023+
PaymentDirection::Inbound,
1024+
PaymentStatus::Pending,
1025+
);
1026+
1027+
match self.payment_store.insert(payment) {
1028+
Ok(_) => {},
1029+
Err(e) => {
1030+
log_error!(
1031+
self.logger,
1032+
"Failed to insert payment with ID {}: {}",
1033+
payment_id,
1034+
e
1035+
);
1036+
return Err(ReplayEvent());
1037+
},
1038+
}
1039+
}
8631040
payment_preimage
8641041
},
8651042
PaymentPurpose::Bolt12OfferPayment {
@@ -998,43 +1175,85 @@ where
9981175
amount_msat,
9991176
);
10001177

1001-
let update = match purpose {
1178+
let (update, kind_for_insert) = match purpose {
10021179
PaymentPurpose::Bolt11InvoicePayment {
10031180
payment_preimage,
10041181
payment_secret,
10051182
..
1006-
} => PaymentDetailsUpdate {
1007-
preimage: Some(payment_preimage),
1008-
secret: Some(Some(payment_secret)),
1009-
amount_msat: Some(Some(amount_msat)),
1010-
status: Some(PaymentStatus::Succeeded),
1011-
..PaymentDetailsUpdate::new(payment_id)
1183+
} => {
1184+
let kind = PaymentKind::Bolt11 {
1185+
hash: payment_hash,
1186+
preimage: payment_preimage,
1187+
secret: Some(payment_secret.clone()),
1188+
};
1189+
let update = PaymentDetailsUpdate {
1190+
preimage: Some(payment_preimage),
1191+
secret: Some(Some(payment_secret)),
1192+
amount_msat: Some(Some(amount_msat)),
1193+
status: Some(PaymentStatus::Succeeded),
1194+
..PaymentDetailsUpdate::new(payment_id)
1195+
};
1196+
(update, kind)
10121197
},
10131198
PaymentPurpose::Bolt12OfferPayment {
1014-
payment_preimage, payment_secret, ..
1015-
} => PaymentDetailsUpdate {
1016-
preimage: Some(payment_preimage),
1017-
secret: Some(Some(payment_secret)),
1018-
amount_msat: Some(Some(amount_msat)),
1019-
status: Some(PaymentStatus::Succeeded),
1020-
..PaymentDetailsUpdate::new(payment_id)
1199+
payment_preimage,
1200+
payment_secret,
1201+
payment_context,
1202+
..
1203+
} => {
1204+
let payer_note = payment_context.invoice_request.payer_note_truncated;
1205+
let offer_id = payment_context.offer_id;
1206+
let quantity = payment_context.invoice_request.quantity;
1207+
let kind = PaymentKind::Bolt12Offer {
1208+
hash: Some(payment_hash),
1209+
preimage: payment_preimage,
1210+
secret: Some(payment_secret.clone()),
1211+
offer_id,
1212+
payer_note,
1213+
quantity,
1214+
};
1215+
let update = PaymentDetailsUpdate {
1216+
preimage: Some(payment_preimage),
1217+
secret: Some(Some(payment_secret)),
1218+
amount_msat: Some(Some(amount_msat)),
1219+
status: Some(PaymentStatus::Succeeded),
1220+
..PaymentDetailsUpdate::new(payment_id)
1221+
};
1222+
(update, kind)
10211223
},
10221224
PaymentPurpose::Bolt12RefundPayment {
10231225
payment_preimage,
10241226
payment_secret,
10251227
..
1026-
} => PaymentDetailsUpdate {
1027-
preimage: Some(payment_preimage),
1028-
secret: Some(Some(payment_secret)),
1029-
amount_msat: Some(Some(amount_msat)),
1030-
status: Some(PaymentStatus::Succeeded),
1031-
..PaymentDetailsUpdate::new(payment_id)
1228+
} => {
1229+
let kind = PaymentKind::Bolt12Refund {
1230+
hash: Some(payment_hash),
1231+
preimage: payment_preimage,
1232+
secret: Some(payment_secret.clone()),
1233+
payer_note: None,
1234+
quantity: None,
1235+
};
1236+
let update = PaymentDetailsUpdate {
1237+
preimage: Some(payment_preimage),
1238+
secret: Some(Some(payment_secret)),
1239+
amount_msat: Some(Some(amount_msat)),
1240+
status: Some(PaymentStatus::Succeeded),
1241+
..PaymentDetailsUpdate::new(payment_id)
1242+
};
1243+
(update, kind)
10321244
},
1033-
PaymentPurpose::SpontaneousPayment(preimage) => PaymentDetailsUpdate {
1034-
preimage: Some(Some(preimage)),
1035-
amount_msat: Some(Some(amount_msat)),
1036-
status: Some(PaymentStatus::Succeeded),
1037-
..PaymentDetailsUpdate::new(payment_id)
1245+
PaymentPurpose::SpontaneousPayment(preimage) => {
1246+
let kind = PaymentKind::Spontaneous {
1247+
hash: payment_hash,
1248+
preimage: Some(preimage),
1249+
};
1250+
let update = PaymentDetailsUpdate {
1251+
preimage: Some(Some(preimage)),
1252+
amount_msat: Some(Some(amount_msat)),
1253+
status: Some(PaymentStatus::Succeeded),
1254+
..PaymentDetailsUpdate::new(payment_id)
1255+
};
1256+
(update, kind)
10381257
},
10391258
};
10401259

@@ -1044,11 +1263,27 @@ where
10441263
// be the result of a replayed event.
10451264
),
10461265
Ok(DataStoreUpdateResult::NotFound) => {
1047-
log_error!(
1048-
self.logger,
1049-
"Claimed payment with ID {} couldn't be found in store",
1266+
// Payment was auto-claimed without a prior store entry.
1267+
let payment = PaymentDetails::new(
10501268
payment_id,
1269+
kind_for_insert,
1270+
Some(amount_msat),
1271+
None,
1272+
PaymentDirection::Inbound,
1273+
PaymentStatus::Succeeded,
10511274
);
1275+
match self.payment_store.insert(payment) {
1276+
Ok(_) => (),
1277+
Err(e) => {
1278+
log_error!(
1279+
self.logger,
1280+
"Failed to insert payment with ID {}: {}",
1281+
payment_id,
1282+
e
1283+
);
1284+
return Err(ReplayEvent());
1285+
},
1286+
}
10521287
},
10531288
Err(e) => {
10541289
log_error!(

tests/common/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,11 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
952952
});
953953
assert_eq!(outbound_payments_b.len(), 0);
954954

955+
// Wait for events before checking inbound payments, as they are now created on demand
956+
// by the event handler.
957+
expect_event!(node_a, PaymentSuccessful);
958+
expect_event!(node_b, PaymentReceived);
959+
955960
let inbound_payments_b = node_b.list_payments_with_filter(|p| {
956961
p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. })
957962
});
@@ -1202,9 +1207,12 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
12021207
node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(),
12031208
5
12041209
);
1210+
// Note: node_b has 5 (not 6) Bolt11 payments because the receive() invoice used only for the
1211+
// underpaid attempt (which fails with InvalidAmount) no longer creates a store entry. Only
1212+
// invoices that result in actual payment events are tracked.
12051213
assert_eq!(
12061214
node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(),
1207-
6
1215+
5
12081216
);
12091217
assert_eq!(
12101218
node_a

0 commit comments

Comments
 (0)