Skip to content

Commit 22abd3b

Browse files
Expose PaidBolt12Invoice in PaymentSuccessful for proof of payment
After a successful BOLT12 payment, users need access to the paid invoice to provide proof of payment to third parties. The invoice contains the payment_hash that can be verified against sha256(preimage). This adds a `bolt12_invoice` field to `PaymentSuccessful` containing the `PaidBolt12Invoice` which wraps either a regular `Bolt12Invoice` or a `StaticInvoice` for async payments. For UniFFI bindings, `PaidBolt12Invoice` is exposed as a dictionary struct that delegates serialization to LDK's native type. For non-UniFFI builds, we simply re-export LDK's `PaidBolt12Invoice` enum directly. Signed-off-by: Vincenzo Palazzo <[email protected]>
1 parent 1f51948 commit 22abd3b

File tree

7 files changed

+317
-8
lines changed

7 files changed

+317
-8
lines changed

bindings/ldk_node.udl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ enum VssHeaderProviderError {
400400

401401
[Enum]
402402
interface Event {
403-
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat);
403+
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat, PaidBolt12Invoice? bolt12_invoice);
404404
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
405405
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
406406
PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence<CustomTlvRecord> custom_records);
@@ -857,6 +857,30 @@ interface Bolt12Invoice {
857857
sequence<u8> encode();
858858
};
859859

860+
interface StaticInvoice {
861+
[Throws=NodeError, Name=from_str]
862+
constructor([ByRef] string invoice_str);
863+
OfferId offer_id();
864+
boolean is_offer_expired();
865+
PublicKey signing_pubkey();
866+
PublicKey? issuer_signing_pubkey();
867+
string? invoice_description();
868+
string? issuer();
869+
OfferAmount? amount();
870+
sequence<u8> chain();
871+
sequence<u8>? metadata();
872+
u64? absolute_expiry_seconds();
873+
sequence<u8> encode();
874+
};
875+
876+
// Note: UniFFI doesn't support Object types in enum variant data, so we use
877+
// a dictionary with optional fields. Check which field is Some to determine
878+
// the invoice type.
879+
dictionary PaidBolt12Invoice {
880+
Bolt12Invoice? bolt12_invoice;
881+
StaticInvoice? static_invoice;
882+
};
883+
860884
[Custom]
861885
typedef string Txid;
862886

src/event.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ use crate::payment::store::{
4848
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
4949
};
5050
use crate::runtime::Runtime;
51-
use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet};
51+
use crate::types::{
52+
CustomTlvRecord, DynStore, OnionMessenger, PaidBolt12Invoice, PaymentStore, Sweeper, Wallet,
53+
};
5254
use crate::{
5355
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
5456
UserChannelId,
@@ -75,6 +77,17 @@ pub enum Event {
7577
payment_preimage: Option<PaymentPreimage>,
7678
/// The total fee which was spent at intermediate hops in this payment.
7779
fee_paid_msat: Option<u64>,
80+
/// The BOLT12 invoice that was paid.
81+
///
82+
/// This is useful for proof of payment. A third party can verify that the payment was made
83+
/// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`.
84+
///
85+
/// Will be `None` for non-BOLT12 payments.
86+
///
87+
/// Note that static invoices (indicated by [`PaidBolt12Invoice::StaticInvoice`], used for
88+
/// async payments) do not support proof of payment as the payment hash is not derived
89+
/// from a preimage known only to the recipient.
90+
bolt12_invoice: Option<PaidBolt12Invoice>,
7891
},
7992
/// A sent payment has failed.
8093
PaymentFailed {
@@ -264,6 +277,7 @@ impl_writeable_tlv_based_enum!(Event,
264277
(1, fee_paid_msat, option),
265278
(3, payment_id, option),
266279
(5, payment_preimage, option),
280+
(7, bolt12_invoice, option),
267281
},
268282
(1, PaymentFailed) => {
269283
(0, payment_hash, option),
@@ -1022,6 +1036,7 @@ where
10221036
payment_preimage,
10231037
payment_hash,
10241038
fee_paid_msat,
1039+
bolt12_invoice,
10251040
..
10261041
} => {
10271042
let payment_id = if let Some(id) = payment_id {
@@ -1062,11 +1077,18 @@ where
10621077
hex_utils::to_string(&payment_preimage.0)
10631078
);
10641079
});
1080+
1081+
// For UniFFI builds, convert LDK's PaidBolt12Invoice to our wrapped type.
1082+
// For non-UniFFI builds, we use LDK's type directly.
1083+
#[cfg(feature = "uniffi")]
1084+
let bolt12_invoice = bolt12_invoice.map(PaidBolt12Invoice::from);
1085+
10651086
let event = Event::PaymentSuccessful {
10661087
payment_id: Some(payment_id),
10671088
payment_hash,
10681089
payment_preimage: Some(payment_preimage),
10691090
fee_paid_msat,
1091+
bolt12_invoice,
10701092
};
10711093

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

src/ffi/types.rs

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@ use bitcoin::hashes::Hash;
2222
use bitcoin::secp256k1::PublicKey;
2323
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txid};
2424
pub use lightning::chain::channelmonitor::BalanceSource;
25+
use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice;
2526
pub use lightning::events::{ClosureReason, PaymentFailureReason};
2627
use lightning::ln::channelmanager::PaymentId;
28+
use lightning::ln::msgs::DecodeError;
2729
pub use lightning::ln::types::ChannelId;
2830
use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice;
2931
pub use lightning::offers::offer::OfferId;
3032
use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer};
3133
use lightning::offers::refund::Refund as LdkRefund;
34+
use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice;
3235
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
3336
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3437
pub use lightning::routing::router::RouteParametersConfig;
35-
use lightning::util::ser::Writeable;
38+
use lightning::util::ser::{Readable, Writeable, Writer};
3639
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
3740
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
3841
pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -686,6 +689,172 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
686689
}
687690
}
688691

692+
/// A `StaticInvoice` is used for async payments where the recipient may be offline.
693+
///
694+
/// Unlike [`Bolt12Invoice`], a `StaticInvoice` does not support proof of payment
695+
/// because the payment hash is not derived from a preimage known only to the recipient.
696+
#[derive(Debug, Clone, PartialEq, Eq)]
697+
pub struct StaticInvoice {
698+
pub(crate) inner: LdkStaticInvoice,
699+
}
700+
701+
impl StaticInvoice {
702+
pub fn from_str(invoice_str: &str) -> Result<Self, Error> {
703+
invoice_str.parse()
704+
}
705+
706+
/// Returns the [`OfferId`] of the underlying [`Offer`] this invoice corresponds to.
707+
///
708+
/// [`Offer`]: lightning::offers::offer::Offer
709+
pub fn offer_id(&self) -> OfferId {
710+
OfferId(self.inner.offer_id().0)
711+
}
712+
713+
/// Whether the offer this invoice corresponds to has expired.
714+
pub fn is_offer_expired(&self) -> bool {
715+
self.inner.is_offer_expired()
716+
}
717+
718+
/// A typically transient public key corresponding to the key used to sign the invoice.
719+
pub fn signing_pubkey(&self) -> PublicKey {
720+
self.inner.signing_pubkey()
721+
}
722+
723+
/// The public key used by the recipient to sign invoices.
724+
pub fn issuer_signing_pubkey(&self) -> Option<PublicKey> {
725+
self.inner.issuer_signing_pubkey()
726+
}
727+
728+
/// A complete description of the purpose of the originating offer.
729+
pub fn invoice_description(&self) -> Option<String> {
730+
self.inner.description().map(|printable| printable.to_string())
731+
}
732+
733+
/// The issuer of the offer.
734+
pub fn issuer(&self) -> Option<String> {
735+
self.inner.issuer().map(|printable| printable.to_string())
736+
}
737+
738+
/// The minimum amount required for a successful payment of a single item.
739+
pub fn amount(&self) -> Option<OfferAmount> {
740+
self.inner.amount().map(|amount| amount.into())
741+
}
742+
743+
/// The chain that must be used when paying the invoice.
744+
pub fn chain(&self) -> Vec<u8> {
745+
self.inner.chain().to_bytes().to_vec()
746+
}
747+
748+
/// Opaque bytes set by the originating [`Offer`].
749+
///
750+
/// [`Offer`]: lightning::offers::offer::Offer
751+
pub fn metadata(&self) -> Option<Vec<u8>> {
752+
self.inner.metadata().cloned()
753+
}
754+
755+
/// Seconds since the Unix epoch when an invoice should no longer be requested.
756+
///
757+
/// If `None`, the offer does not expire.
758+
pub fn absolute_expiry_seconds(&self) -> Option<u64> {
759+
self.inner.absolute_expiry().map(|duration| duration.as_secs())
760+
}
761+
762+
/// Writes `self` out to a `Vec<u8>`.
763+
pub fn encode(&self) -> Vec<u8> {
764+
self.inner.encode()
765+
}
766+
}
767+
768+
impl std::str::FromStr for StaticInvoice {
769+
type Err = Error;
770+
771+
fn from_str(invoice_str: &str) -> Result<Self, Self::Err> {
772+
if let Some(bytes_vec) = hex_utils::to_vec(invoice_str) {
773+
if let Ok(invoice) = LdkStaticInvoice::try_from(bytes_vec) {
774+
return Ok(StaticInvoice { inner: invoice });
775+
}
776+
}
777+
Err(Error::InvalidInvoice)
778+
}
779+
}
780+
781+
impl From<LdkStaticInvoice> for StaticInvoice {
782+
fn from(invoice: LdkStaticInvoice) -> Self {
783+
StaticInvoice { inner: invoice }
784+
}
785+
}
786+
787+
impl Deref for StaticInvoice {
788+
type Target = LdkStaticInvoice;
789+
fn deref(&self) -> &Self::Target {
790+
&self.inner
791+
}
792+
}
793+
794+
impl AsRef<LdkStaticInvoice> for StaticInvoice {
795+
fn as_ref(&self) -> &LdkStaticInvoice {
796+
self.deref()
797+
}
798+
}
799+
800+
/// Represents a BOLT12 invoice that was paid.
801+
///
802+
/// This is used in [`Event::PaymentSuccessful`] to provide proof of payment for BOLT12 payments.
803+
///
804+
/// Note: Due to UniFFI limitations with Object types in enum variants, this is exposed as a
805+
/// struct with optional fields. Check which field is `Some` to determine the invoice type:
806+
/// - `bolt12_invoice`: A standard BOLT12 invoice (supports proof of payment)
807+
/// - `static_invoice`: A static invoice for async payments (does NOT support proof of payment)
808+
///
809+
/// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful
810+
#[derive(Debug, Clone, PartialEq, Eq)]
811+
pub struct PaidBolt12Invoice {
812+
/// The paid BOLT12 invoice, if this is a regular BOLT12 invoice.
813+
pub bolt12_invoice: Option<Arc<Bolt12Invoice>>,
814+
/// The paid static invoice, if this is a static invoice (async payment).
815+
pub static_invoice: Option<Arc<StaticInvoice>>,
816+
}
817+
818+
impl From<LdkPaidBolt12Invoice> for PaidBolt12Invoice {
819+
fn from(ldk_invoice: LdkPaidBolt12Invoice) -> Self {
820+
match ldk_invoice {
821+
LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => PaidBolt12Invoice {
822+
bolt12_invoice: Some(super::maybe_wrap(invoice)),
823+
static_invoice: None,
824+
},
825+
LdkPaidBolt12Invoice::StaticInvoice(invoice) => PaidBolt12Invoice {
826+
bolt12_invoice: None,
827+
static_invoice: Some(super::maybe_wrap(invoice)),
828+
},
829+
}
830+
}
831+
}
832+
833+
impl Writeable for PaidBolt12Invoice {
834+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
835+
// Convert our struct back to LDK's enum and delegate serialization
836+
let ldk_invoice: LdkPaidBolt12Invoice = match (&self.bolt12_invoice, &self.static_invoice) {
837+
(Some(inv), None) => LdkPaidBolt12Invoice::Bolt12Invoice(inv.inner.clone()),
838+
(None, Some(inv)) => LdkPaidBolt12Invoice::StaticInvoice(inv.inner.clone()),
839+
_ => {
840+
return Err(lightning::io::Error::new(
841+
lightning::io::ErrorKind::InvalidData,
842+
"PaidBolt12Invoice must have exactly one variant set",
843+
));
844+
},
845+
};
846+
ldk_invoice.write(writer)
847+
}
848+
}
849+
850+
impl Readable for PaidBolt12Invoice {
851+
fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
852+
// Read using LDK's deserialization, then convert to our type
853+
let ldk_invoice: LdkPaidBolt12Invoice = Readable::read(reader)?;
854+
Ok(PaidBolt12Invoice::from(ldk_invoice))
855+
}
856+
}
857+
689858
impl UniffiCustomTypeConverter for OfferId {
690859
type Builtin = String;
691860

src/io/utils.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
use std::fs::{self, OpenOptions};
99
use std::io::Write;
1010
use std::ops::Deref;
11-
use std::path::Path;
12-
use std::sync::Arc;
13-
1411
#[cfg(unix)]
1512
use std::os::unix::fs::OpenOptionsExt;
13+
use std::path::Path;
14+
use std::sync::Arc;
1615

1716
use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet;
1817
use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet;

src/payment/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ pub use store::{
2323
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
2424
};
2525
pub use unified::{UnifiedPayment, UnifiedPaymentResult};
26+
27+
pub use crate::types::PaidBolt12Invoice;
28+
#[cfg(feature = "uniffi")]
29+
pub use crate::types::{Bolt12Invoice, StaticInvoice};

src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,12 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
609609
CustomTlvRecord { type_num: tlv.0, value: tlv.1.clone() }
610610
}
611611
}
612+
613+
// Re-export the invoice types. When UniFFI is enabled, we use the types from ffi::types
614+
// which have additional UniFFI-specific implementations. Otherwise, we re-export LDK's
615+
// types directly to avoid unnecessary wrappers and serialization reimplementation.
616+
#[cfg(not(feature = "uniffi"))]
617+
pub use lightning::events::PaidBolt12Invoice;
618+
619+
#[cfg(feature = "uniffi")]
620+
pub use crate::ffi::{Bolt12Invoice, PaidBolt12Invoice, StaticInvoice};

0 commit comments

Comments
 (0)