Skip to content

Commit f812d84

Browse files
Persist the paid BOLT 12 invoice in the payment details
Add a `bolt12_invoice` field to `PaymentKind::Bolt12Offer` and `PaymentKind::Bolt12Refund`, and set it from `Event::PaymentSent` when a BOLT 12 payment succeeds. Besides being useful on its own, this gives us everything we need to build a payer proof for a past payment without keeping any additional state around: the invoice is persisted alongside the payment and thus survives restarts. This commit was written with AI assistance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 52061f5 commit f812d84

4 files changed

Lines changed: 74 additions & 10 deletions

File tree

src/event.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,7 @@ where
11291129
offer_id,
11301130
payer_note,
11311131
quantity,
1132+
bolt12_invoice: None,
11321133
};
11331134

11341135
let payment = PaymentDetails::new(
@@ -1178,6 +1179,7 @@ where
11781179
secret: Some(payment_secret),
11791180
payer_note: None,
11801181
quantity: None,
1182+
bolt12_invoice: None,
11811183
};
11821184

11831185
let payment = PaymentDetails::new(
@@ -1331,6 +1333,7 @@ where
13311333
offer_id: payment_context.offer_id,
13321334
payer_note: payment_context.invoice_request.payer_note_truncated,
13331335
quantity: payment_context.invoice_request.quantity,
1336+
bolt12_invoice: None,
13341337
};
13351338
let update = PaymentDetailsUpdate {
13361339
preimage: Some(payment_preimage),
@@ -1352,6 +1355,7 @@ where
13521355
secret: Some(payment_secret),
13531356
payer_note: None,
13541357
quantity: None,
1358+
bolt12_invoice: None,
13551359
};
13561360
let update = PaymentDetailsUpdate {
13571361
preimage: Some(payment_preimage),
@@ -1442,12 +1446,16 @@ where
14421446
debug_assert!(false, "payment_id should always be set.");
14431447
return Ok(());
14441448
};
1449+
let bolt12_invoice = bolt12_invoice.map(Into::into);
14451450

1451+
// Only set the field if the event actually carried an invoice, to avoid
1452+
// overriding any previously-stored invoice with `None`.
14461453
let update = PaymentDetailsUpdate {
14471454
hash: Some(Some(payment_hash)),
14481455
preimage: Some(Some(payment_preimage)),
14491456
fee_paid_msat: Some(fee_paid_msat),
14501457
status: Some(PaymentStatus::Succeeded),
1458+
bolt12_invoice: bolt12_invoice.clone().map(Some),
14511459
..PaymentDetailsUpdate::new(payment_id)
14521460
};
14531461

@@ -1482,7 +1490,7 @@ where
14821490
payment_hash,
14831491
payment_preimage: Some(payment_preimage),
14841492
fee_paid_msat,
1485-
bolt12_invoice: bolt12_invoice.map(Into::into),
1493+
bolt12_invoice,
14861494
};
14871495

14881496
match self.event_queue.add_event(event).await {

src/payment/bolt12.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ impl Bolt12Payment {
158158
offer_id: offer.id(),
159159
payer_note: payer_note.map(UntrustedString),
160160
quantity,
161+
bolt12_invoice: None,
161162
};
162163
let payment = PaymentDetails::new(
163164
payment_id,
@@ -183,6 +184,7 @@ impl Bolt12Payment {
183184
offer_id: offer.id(),
184185
payer_note: payer_note.map(UntrustedString),
185186
quantity,
187+
bolt12_invoice: None,
186188
};
187189
let payment = PaymentDetails::new(
188190
payment_id,
@@ -320,6 +322,7 @@ impl Bolt12Payment {
320322
offer_id: offer.id(),
321323
payer_note: payer_note.map(UntrustedString),
322324
quantity,
325+
bolt12_invoice: None,
323326
};
324327
let payment = PaymentDetails::new(
325328
payment_id,
@@ -345,6 +348,7 @@ impl Bolt12Payment {
345348
offer_id: offer.id(),
346349
payer_note: payer_note.map(UntrustedString),
347350
quantity,
351+
bolt12_invoice: None,
348352
};
349353
let payment = PaymentDetails::new(
350354
payment_id,
@@ -498,6 +502,7 @@ impl Bolt12Payment {
498502
secret: None,
499503
payer_note: payer_note.map(|note| UntrustedString(note)),
500504
quantity,
505+
bolt12_invoice: None,
501506
};
502507
let payment = PaymentDetails::new(
503508
payment_id,

src/payment/store.rs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
1010
use bitcoin::secp256k1::PublicKey;
1111
use bitcoin::{BlockHash, Txid};
1212
use lightning::chain::chaininterface::TransactionType as LdkTransactionType;
13+
#[cfg(not(feature = "uniffi"))]
14+
use lightning::events::PaidBolt12Invoice;
1315
use lightning::ln::channelmanager::PaymentId;
1416
use lightning::ln::msgs::DecodeError;
1517
use lightning::ln::types::ChannelId;
@@ -23,6 +25,8 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
2325
use lightning_types::string::UntrustedString;
2426

2527
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
28+
#[cfg(feature = "uniffi")]
29+
use crate::ffi::PaidBolt12Invoice;
2630
use crate::hex_utils;
2731

2832
/// Represents a payment.
@@ -251,6 +255,18 @@ impl StorableObject for PaymentDetails {
251255
update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt);
252256
}
253257

258+
if let Some(ref bolt12_invoice_opt) = update.bolt12_invoice {
259+
match self.kind {
260+
PaymentKind::Bolt12Offer { ref mut bolt12_invoice, .. } => {
261+
update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone());
262+
},
263+
PaymentKind::Bolt12Refund { ref mut bolt12_invoice, .. } => {
264+
update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone());
265+
},
266+
_ => {},
267+
}
268+
}
269+
254270
if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat {
255271
match self.kind {
256272
PaymentKind::Bolt11 { ref mut counterparty_skimmed_fee_msat, .. } => {
@@ -559,6 +575,15 @@ pub enum PaymentKind {
559575
///
560576
/// This will always be `None` for payments serialized with version `v0.3.0`.
561577
quantity: Option<u64>,
578+
/// The BOLT 12 invoice that was paid, set once the payment succeeded.
579+
///
580+
/// Only set for successful outbound payments, allowing to build a payer proof for the
581+
/// payment via [`Bolt12Payment::create_payer_proof`].
582+
///
583+
/// This will always be `None` for payments serialized with version `v0.7.0` or prior.
584+
///
585+
/// [`Bolt12Payment::create_payer_proof`]: crate::payment::Bolt12Payment::create_payer_proof
586+
bolt12_invoice: Option<PaidBolt12Invoice>,
562587
},
563588
/// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`].
564589
///
@@ -586,6 +611,15 @@ pub enum PaymentKind {
586611
///
587612
/// This will always be `None` for payments serialized with version `v0.3.0`.
588613
quantity: Option<u64>,
614+
/// The BOLT 12 invoice that was paid, set once the payment succeeded.
615+
///
616+
/// Only set for successful outbound payments, allowing to build a payer proof for the
617+
/// payment via [`Bolt12Payment::create_payer_proof`].
618+
///
619+
/// This will always be `None` for payments serialized with version `v0.7.0` or prior.
620+
///
621+
/// [`Bolt12Payment::create_payer_proof`]: crate::payment::Bolt12Payment::create_payer_proof
622+
bolt12_invoice: Option<PaidBolt12Invoice>,
589623
},
590624
/// A spontaneous ("keysend") payment.
591625
Spontaneous {
@@ -625,6 +659,7 @@ impl_writeable_tlv_based_enum!(PaymentKind,
625659
(3, quantity, option),
626660
(4, secret, option),
627661
(6, offer_id, required),
662+
(8, bolt12_invoice, option),
628663
},
629664
(8, Spontaneous) => {
630665
(0, hash, required),
@@ -636,6 +671,7 @@ impl_writeable_tlv_based_enum!(PaymentKind,
636671
(2, preimage, option),
637672
(3, quantity, option),
638673
(4, secret, option),
674+
(6, bolt12_invoice, option),
639675
}
640676
);
641677

@@ -698,6 +734,7 @@ pub(crate) struct PaymentDetailsUpdate {
698734
pub direction: Option<PaymentDirection>,
699735
pub status: Option<PaymentStatus>,
700736
pub confirmation_status: Option<ConfirmationStatus>,
737+
pub bolt12_invoice: Option<Option<PaidBolt12Invoice>>,
701738
pub txid: Option<Txid>,
702739
pub tx_type: Option<Option<TransactionType>>,
703740
}
@@ -715,6 +752,7 @@ impl PaymentDetailsUpdate {
715752
direction: None,
716753
status: None,
717754
confirmation_status: None,
755+
bolt12_invoice: None,
718756
txid: None,
719757
tx_type: None,
720758
}
@@ -723,12 +761,18 @@ impl PaymentDetailsUpdate {
723761

724762
impl From<&PaymentDetails> for PaymentDetailsUpdate {
725763
fn from(value: &PaymentDetails) -> Self {
726-
let (hash, preimage, secret) = match value.kind {
727-
PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret),
728-
PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret),
729-
PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret),
730-
PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None),
731-
_ => (None, None, None),
764+
let (hash, preimage, secret, bolt12_invoice) = match &value.kind {
765+
PaymentKind::Bolt11 { hash, preimage, secret, .. } => {
766+
(Some(*hash), *preimage, *secret, None)
767+
},
768+
PaymentKind::Bolt12Offer { hash, preimage, secret, bolt12_invoice, .. } => {
769+
(*hash, *preimage, *secret, Some(bolt12_invoice.clone()))
770+
},
771+
PaymentKind::Bolt12Refund { hash, preimage, secret, bolt12_invoice, .. } => {
772+
(*hash, *preimage, *secret, Some(bolt12_invoice.clone()))
773+
},
774+
PaymentKind::Spontaneous { hash, preimage, .. } => (Some(*hash), *preimage, None, None),
775+
_ => (None, None, None, None),
732776
};
733777

734778
let (confirmation_status, txid, tx_type) = match &value.kind {
@@ -738,9 +782,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
738782
_ => (None, None, None),
739783
};
740784

741-
let counterparty_skimmed_fee_msat = match value.kind {
785+
let counterparty_skimmed_fee_msat = match &value.kind {
742786
PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => {
743-
Some(counterparty_skimmed_fee_msat)
787+
Some(*counterparty_skimmed_fee_msat)
744788
},
745789
_ => None,
746790
};
@@ -756,6 +800,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
756800
direction: Some(value.direction),
757801
status: Some(value.status),
758802
confirmation_status,
803+
bolt12_invoice,
759804
txid,
760805
tx_type,
761806
}

tests/integration_tests_rust.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2329,12 +2329,14 @@ async fn simple_bolt12_send_receive() {
23292329
offer_id,
23302330
quantity: ref qty,
23312331
payer_note: ref note,
2332+
bolt12_invoice: ref invoice,
23322333
} => {
23332334
assert!(hash.is_some());
23342335
assert!(preimage.is_some());
23352336
assert_eq!(offer_id, offer.id());
23362337
assert_eq!(&expected_quantity, qty);
23372338
assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0);
2339+
assert!(invoice.is_some());
23382340
// TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12
23392341
// API currently doesn't allow to do that.
23402342
},
@@ -2396,12 +2398,14 @@ async fn simple_bolt12_send_receive() {
23962398
offer_id,
23972399
quantity: ref qty,
23982400
payer_note: ref note,
2401+
bolt12_invoice: ref invoice,
23992402
} => {
24002403
assert!(hash.is_some());
24012404
assert!(preimage.is_some());
24022405
assert_eq!(offer_id, offer.id());
24032406
assert_eq!(&expected_quantity, qty);
24042407
assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0);
2408+
assert!(invoice.is_some());
24052409
// TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12
24062410
// API currently doesn't allow to do that.
24072411
},
@@ -2467,11 +2471,13 @@ async fn simple_bolt12_send_receive() {
24672471
secret: _,
24682472
quantity: ref qty,
24692473
payer_note: ref note,
2474+
bolt12_invoice: ref invoice,
24702475
} => {
24712476
assert!(hash.is_some());
24722477
assert!(preimage.is_some());
24732478
assert_eq!(&expected_quantity, qty);
2474-
assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0)
2479+
assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0);
2480+
assert!(invoice.is_some());
24752481
// TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12
24762482
// API currently doesn't allow to do that.
24772483
},

0 commit comments

Comments
 (0)