Skip to content

Commit e15e44b

Browse files
event: expose paid BOLT12 invoice in PaymentSuccessful
Expose the paid BOLT12 invoice on successful outbound payments so API users can access the invoice alongside the payment preimage. This keeps the event surface needed for follow-up storage and payer-proof work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> AI-assisted-by: Codex
1 parent a8b956a commit e15e44b

4 files changed

Lines changed: 140 additions & 7 deletions

File tree

src/event.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use bitcoin::blockdata::locktime::absolute::LockTime;
1515
use bitcoin::secp256k1::PublicKey;
1616
use bitcoin::{Amount, OutPoint};
1717
use lightning::events::bump_transaction::BumpTransactionEvent;
18+
#[cfg(not(feature = "uniffi"))]
19+
use lightning::events::PaidBolt12Invoice;
1820
use lightning::events::{
1921
ClosureReason, Event as LdkEvent, FundingInfo, PaymentFailureReason, PaymentPurpose,
2022
ReplayEvent,
@@ -37,6 +39,8 @@ use crate::config::{may_announce_channel, Config};
3739
use crate::connection::ConnectionManager;
3840
use crate::data_store::DataStoreUpdateResult;
3941
use crate::fee_estimator::ConfirmationTarget;
42+
#[cfg(feature = "uniffi")]
43+
use crate::ffi::PaidBolt12Invoice;
4044
use crate::io::{
4145
EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE,
4246
EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE,
@@ -79,6 +83,17 @@ pub enum Event {
7983
payment_preimage: Option<PaymentPreimage>,
8084
/// The total fee which was spent at intermediate hops in this payment.
8185
fee_paid_msat: Option<u64>,
86+
/// The BOLT12 invoice that was paid.
87+
///
88+
/// This is useful for proof of payment. A third party can verify that the payment was made
89+
/// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`.
90+
///
91+
/// Will be `None` for non-BOLT12 payments.
92+
///
93+
/// Note that static invoices (indicated by [`PaidBolt12Invoice::StaticInvoice`], used for
94+
/// async payments) do not support proof of payment as the payment hash is not derived
95+
/// from a preimage known only to the recipient.
96+
bolt12_invoice: Option<PaidBolt12Invoice>,
8297
},
8398
/// A sent payment has failed.
8499
PaymentFailed {
@@ -268,6 +283,7 @@ impl_writeable_tlv_based_enum!(Event,
268283
(1, fee_paid_msat, option),
269284
(3, payment_id, option),
270285
(5, payment_preimage, option),
286+
(7, bolt12_invoice, option),
271287
},
272288
(1, PaymentFailed) => {
273289
(0, payment_hash, option),
@@ -1028,6 +1044,7 @@ where
10281044
payment_preimage,
10291045
payment_hash,
10301046
fee_paid_msat,
1047+
bolt12_invoice,
10311048
..
10321049
} => {
10331050
let payment_id = if let Some(id) = payment_id {
@@ -1073,6 +1090,7 @@ where
10731090
payment_hash,
10741091
payment_preimage: Some(payment_preimage),
10751092
fee_paid_msat,
1093+
bolt12_invoice: bolt12_invoice.map(Into::into),
10761094
};
10771095

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

src/ffi/types.rs

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
// Make sure to add any re-exported items that need to be used in uniffi below.
1212

13+
use std::collections::HashMap;
1314
use std::convert::TryInto;
1415
use std::ops::Deref;
1516
use std::str::FromStr;
@@ -22,17 +23,20 @@ use bitcoin::hashes::Hash;
2223
use bitcoin::secp256k1::PublicKey;
2324
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txid};
2425
pub use lightning::chain::channelmonitor::BalanceSource;
26+
use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice;
2527
pub use lightning::events::{ClosureReason, PaymentFailureReason};
2628
use lightning::ln::channelmanager::PaymentId;
29+
use lightning::ln::msgs::DecodeError;
2730
pub use lightning::ln::types::ChannelId;
2831
use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice;
2932
pub use lightning::offers::offer::OfferId;
3033
use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer};
3134
use lightning::offers::refund::Refund as LdkRefund;
35+
use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice;
3236
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
3337
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3438
pub use lightning::routing::router::RouteParametersConfig;
35-
use lightning::util::ser::Writeable;
39+
use lightning::util::ser::{Readable, Writeable, Writer};
3640
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
3741
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
3842
pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -41,10 +45,10 @@ pub use lightning_liquidity::lsps1::msgs::{
4145
};
4246
pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
4347
pub use lightning_types::string::UntrustedString;
44-
use std::collections::HashMap;
45-
46-
use vss_client::headers::VssHeaderProvider as VssClientHeaderProvider;
47-
use vss_client::headers::VssHeaderProviderError as VssClientHeaderProviderError;
48+
use vss_client::headers::{
49+
VssHeaderProvider as VssClientHeaderProvider,
50+
VssHeaderProviderError as VssClientHeaderProviderError,
51+
};
4852

4953
/// Errors around providing headers for each VSS request.
5054
#[derive(Debug, uniffi::Error)]
@@ -775,6 +779,95 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
775779
}
776780
}
777781

782+
/// A static invoice used for async payments.
783+
///
784+
/// Static invoices are a special type of BOLT12 invoice where proof of payment is not possible,
785+
/// as the payment hash is not derived from a preimage known only to the recipient.
786+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)]
787+
pub struct StaticInvoice {
788+
pub(crate) inner: LdkStaticInvoice,
789+
}
790+
791+
#[uniffi::export]
792+
impl StaticInvoice {
793+
/// The amount for a successful payment of the invoice, if specified.
794+
pub fn amount(&self) -> Option<OfferAmount> {
795+
self.inner.amount().map(|amount| amount.into())
796+
}
797+
}
798+
799+
impl From<LdkStaticInvoice> for StaticInvoice {
800+
fn from(invoice: LdkStaticInvoice) -> Self {
801+
StaticInvoice { inner: invoice }
802+
}
803+
}
804+
805+
impl Deref for StaticInvoice {
806+
type Target = LdkStaticInvoice;
807+
fn deref(&self) -> &Self::Target {
808+
&self.inner
809+
}
810+
}
811+
812+
impl AsRef<LdkStaticInvoice> for StaticInvoice {
813+
fn as_ref(&self) -> &LdkStaticInvoice {
814+
self.deref()
815+
}
816+
}
817+
818+
/// The BOLT12 invoice that was paid, surfaced in [`Event::PaymentSuccessful`].
819+
///
820+
/// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful
821+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
822+
pub enum PaidBolt12Invoice {
823+
/// The BOLT12 invoice, allowing the user to perform proof of payment.
824+
Bolt12(Arc<Bolt12Invoice>),
825+
/// The static invoice, used in async payments, where the user cannot perform proof of
826+
/// payment.
827+
Static(Arc<StaticInvoice>),
828+
}
829+
830+
impl From<LdkPaidBolt12Invoice> for PaidBolt12Invoice {
831+
fn from(ldk: LdkPaidBolt12Invoice) -> Self {
832+
match ldk {
833+
LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => {
834+
PaidBolt12Invoice::Bolt12(Arc::new(Bolt12Invoice::from(invoice)))
835+
},
836+
LdkPaidBolt12Invoice::StaticInvoice(invoice) => {
837+
PaidBolt12Invoice::Static(Arc::new(StaticInvoice::from(invoice)))
838+
},
839+
}
840+
}
841+
}
842+
843+
impl From<PaidBolt12Invoice> for LdkPaidBolt12Invoice {
844+
fn from(wrapper: PaidBolt12Invoice) -> Self {
845+
match wrapper {
846+
PaidBolt12Invoice::Bolt12(invoice) => {
847+
LdkPaidBolt12Invoice::Bolt12Invoice(invoice.inner.clone())
848+
},
849+
PaidBolt12Invoice::Static(invoice) => {
850+
LdkPaidBolt12Invoice::StaticInvoice(invoice.inner.clone())
851+
},
852+
}
853+
}
854+
}
855+
856+
impl Writeable for PaidBolt12Invoice {
857+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), lightning::io::Error> {
858+
// TODO: Find way to avoid cloning invoice data.
859+
let ldk_type: LdkPaidBolt12Invoice = self.clone().into();
860+
ldk_type.write(w)
861+
}
862+
}
863+
864+
impl Readable for PaidBolt12Invoice {
865+
fn read<R: lightning::io::Read>(r: &mut R) -> Result<Self, DecodeError> {
866+
let ldk_type = LdkPaidBolt12Invoice::read(r)?;
867+
Ok(ldk_type.into())
868+
}
869+
}
870+
778871
uniffi::custom_type!(OfferId, String, {
779872
remote,
780873
try_lift: |val| {

tests/common/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,17 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
953953
});
954954
assert_eq!(inbound_payments_b.len(), 1);
955955

956-
expect_event!(node_a, PaymentSuccessful);
956+
// Verify bolt12_invoice is None for BOLT11 payments
957+
match node_a.next_event_async().await {
958+
ref e @ Event::PaymentSuccessful { ref bolt12_invoice, .. } => {
959+
println!("{} got event {:?}", node_a.node_id(), e);
960+
assert!(bolt12_invoice.is_none(), "bolt12_invoice should be None for BOLT11 payments");
961+
node_a.event_handled().unwrap();
962+
},
963+
ref e => {
964+
panic!("{} got unexpected event!: {:?}", std::stringify!(node_a), e);
965+
},
966+
}
957967
expect_event!(node_b, PaymentReceived);
958968
assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded);
959969
assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound);

tests/integration_tests_rust.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,19 @@ async fn simple_bolt12_send_receive() {
10891089
.send(&offer, expected_quantity, expected_payer_note.clone(), None)
10901090
.unwrap();
10911091

1092-
expect_payment_successful_event!(node_a, Some(payment_id), None);
1092+
let event = node_a.next_event_async().await;
1093+
match event {
1094+
ref e @ Event::PaymentSuccessful { payment_id: ref evt_id, ref bolt12_invoice, .. } => {
1095+
println!("{} got event {:?}", node_a.node_id(), e);
1096+
assert_eq!(*evt_id, Some(payment_id));
1097+
assert!(
1098+
bolt12_invoice.is_some(),
1099+
"bolt12_invoice should be present for BOLT12 payments"
1100+
);
1101+
node_a.event_handled().unwrap();
1102+
},
1103+
ref e => panic!("{} got unexpected event!: {:?}", "node_a", e),
1104+
}
10931105
let node_a_payments =
10941106
node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. }));
10951107
assert_eq!(node_a_payments.len(), 1);

0 commit comments

Comments
 (0)