Skip to content

Commit 025fea4

Browse files
event: expose PaidBolt12Invoice in PaymentSuccessful for proof of payment
Add the `bolt12_invoice` field to the `PaymentSuccessful` event, enabling users to obtain proof of payment for BOLT12 transactions. Problem: After a successful BOLT12 payment, users had no way to access the paid invoice data. This made it impossible to provide proof of payment to third parties, who need both the payment preimage and the original invoice to verify that sha256(preimage) matches the invoice's payment_hash. Solution: Add `bolt12_invoice: Option<PaidBolt12Invoice>` to `PaymentSuccessful`. With the UniFFI v0.29 upgrade now supporting objects in enum variants, we can expose the proper `PaidBolt12Invoice` type across both native Rust and FFI builds without cfg-gating the Event field. For non-UniFFI builds, LDK's `PaidBolt12Invoice` is re-exported directly. For UniFFI builds, a wrapper `PaidBolt12Invoice` enum is defined in ffi/types.rs with `From` conversions and delegating serialization. A minimal `StaticInvoice` FFI wrapper is also added to support the `PaidBolt12Invoice::StaticInvoice` variant. TLV tag 7 is used for serialization, maintaining backward compatibility: older readers silently skip the unknown odd tag, and newer readers deserialize `None` from events without it. Closes #757. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 87c16eb commit 025fea4

File tree

5 files changed

+202
-3
lines changed

5 files changed

+202
-3
lines changed

src/event.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ use crate::config::{may_announce_channel, Config};
3737
use crate::connection::ConnectionManager;
3838
use crate::data_store::DataStoreUpdateResult;
3939
use crate::fee_estimator::ConfirmationTarget;
40+
#[cfg(feature = "uniffi")]
41+
use crate::ffi::PaidBolt12Invoice;
4042
use crate::io::{
4143
EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE,
4244
EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE,
@@ -49,6 +51,8 @@ use crate::payment::store::{
4951
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
5052
};
5153
use crate::runtime::Runtime;
54+
#[cfg(not(feature = "uniffi"))]
55+
use crate::types::PaidBolt12Invoice;
5256
use crate::types::{
5357
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet,
5458
};
@@ -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: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@ use bitcoin::hashes::Hash;
2323
use bitcoin::secp256k1::PublicKey;
2424
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txid};
2525
pub use lightning::chain::channelmonitor::BalanceSource;
26+
use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice;
2627
pub use lightning::events::{ClosureReason, PaymentFailureReason};
2728
use lightning::ln::channelmanager::PaymentId;
29+
use lightning::ln::msgs::DecodeError;
2830
pub use lightning::ln::types::ChannelId;
2931
use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice;
3032
pub use lightning::offers::offer::OfferId;
3133
use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer};
3234
use lightning::offers::refund::Refund as LdkRefund;
35+
use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice;
3336
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
3437
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3538
pub use lightning::routing::router::RouteParametersConfig;
36-
use lightning::util::ser::Writeable;
39+
use lightning::util::ser::{Readable, Writeable, Writer};
3740
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
3841
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
3942
pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -776,6 +779,94 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
776779
}
777780
}
778781

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+
Bolt12Invoice(Arc<Bolt12Invoice>),
825+
/// The static invoice, used in async payments, where the user cannot perform proof of
826+
/// payment.
827+
StaticInvoice(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::Bolt12Invoice(Arc::new(Bolt12Invoice::from(invoice)))
835+
},
836+
LdkPaidBolt12Invoice::StaticInvoice(invoice) => {
837+
PaidBolt12Invoice::StaticInvoice(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::Bolt12Invoice(invoice) => {
847+
LdkPaidBolt12Invoice::Bolt12Invoice(invoice.inner.clone())
848+
},
849+
PaidBolt12Invoice::StaticInvoice(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+
let ldk_type: LdkPaidBolt12Invoice = self.clone().into();
859+
ldk_type.write(w)
860+
}
861+
}
862+
863+
impl Readable for PaidBolt12Invoice {
864+
fn read<R: lightning::io::Read>(r: &mut R) -> Result<Self, DecodeError> {
865+
let ldk_type = LdkPaidBolt12Invoice::read(r)?;
866+
Ok(ldk_type.into())
867+
}
868+
}
869+
779870
uniffi::custom_type!(OfferId, String, {
780871
remote,
781872
try_lift: |val| {

src/payment/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ pub use store::{
2525
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
2626
};
2727
pub use unified::{UnifiedPayment, UnifiedPaymentResult};
28+
29+
#[cfg(feature = "uniffi")]
30+
pub use crate::ffi::PaidBolt12Invoice;
31+
#[cfg(not(feature = "uniffi"))]
32+
pub use crate::types::PaidBolt12Invoice;

src/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,6 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
626626
}
627627

628628
pub(crate) type PendingPaymentStore = DataStore<PendingPaymentDetails, Arc<Logger>>;
629+
630+
#[cfg(not(feature = "uniffi"))]
631+
pub use lightning::events::PaidBolt12Invoice;

tests/integration_tests_rust.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3030
use ldk_node::entropy::NodeEntropy;
3131
use ldk_node::liquidity::LSPS2ServiceConfig;
3232
use ldk_node::payment::{
33-
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
34-
UnifiedPaymentResult,
33+
ConfirmationStatus, PaidBolt12Invoice, PaymentDetails, PaymentDirection, PaymentKind,
34+
PaymentStatus, UnifiedPaymentResult,
3535
};
3636
use ldk_node::{Builder, Event, NodeError};
3737
use lightning::ln::channelmanager::PaymentId;
@@ -1273,6 +1273,88 @@ async fn simple_bolt12_send_receive() {
12731273
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
12741274
}
12751275

1276+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1277+
async fn bolt12_proof_of_payment() {
1278+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1279+
let chain_source = TestChainSource::Esplora(&electrsd);
1280+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1281+
1282+
let address_a = node_a.onchain_payment().new_address().unwrap();
1283+
let premine_amount_sat = 5_000_000;
1284+
premine_and_distribute_funds(
1285+
&bitcoind.client,
1286+
&electrsd.client,
1287+
vec![address_a],
1288+
Amount::from_sat(premine_amount_sat),
1289+
)
1290+
.await;
1291+
1292+
node_a.sync_wallets().unwrap();
1293+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
1294+
1295+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1296+
1297+
node_a.sync_wallets().unwrap();
1298+
node_b.sync_wallets().unwrap();
1299+
1300+
expect_channel_ready_event!(node_a, node_b.node_id());
1301+
expect_channel_ready_event!(node_b, node_a.node_id());
1302+
1303+
// Sleep until we broadcasted a node announcement.
1304+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
1305+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1306+
}
1307+
1308+
// Sleep one more sec to make sure the node announcement propagates.
1309+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
1310+
1311+
let expected_amount_msat = 100_000_000;
1312+
let offer = node_b
1313+
.bolt12_payment()
1314+
.receive(expected_amount_msat, "proof of payment test", None, Some(1))
1315+
.unwrap();
1316+
let payment_id =
1317+
node_a.bolt12_payment().send(&offer, Some(1), Some("Test".to_string()), None).unwrap();
1318+
1319+
// Wait for payment and verify proof of payment
1320+
match node_a.next_event_async().await {
1321+
Event::PaymentSuccessful {
1322+
payment_id: event_payment_id,
1323+
payment_hash,
1324+
payment_preimage,
1325+
fee_paid_msat: _,
1326+
bolt12_invoice,
1327+
} => {
1328+
assert_eq!(event_payment_id, Some(payment_id));
1329+
1330+
// Verify proof of payment: sha256(preimage) == payment_hash
1331+
let preimage = payment_preimage.expect("preimage should be present");
1332+
let computed_hash = Sha256Hash::hash(&preimage.0);
1333+
assert_eq!(PaymentHash(computed_hash.to_byte_array()), payment_hash);
1334+
1335+
// Verify the BOLT12 invoice is present and contains the correct payment hash
1336+
let paid_invoice =
1337+
bolt12_invoice.expect("bolt12_invoice should be present for BOLT12 payments");
1338+
match paid_invoice {
1339+
PaidBolt12Invoice::Bolt12Invoice(invoice) => {
1340+
assert_eq!(invoice.payment_hash(), payment_hash);
1341+
assert_eq!(invoice.amount_msats(), expected_amount_msat);
1342+
},
1343+
PaidBolt12Invoice::StaticInvoice(_) => {
1344+
panic!("Expected Bolt12Invoice, got StaticInvoice");
1345+
},
1346+
}
1347+
1348+
node_a.event_handled().unwrap();
1349+
},
1350+
ref e => {
1351+
panic!("Unexpected event: {:?}", e);
1352+
},
1353+
}
1354+
1355+
expect_payment_received_event!(node_b, expected_amount_msat);
1356+
}
1357+
12761358
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
12771359
async fn async_payment() {
12781360
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)