Skip to content

Commit a3a60b3

Browse files
ffi: expose PaidBolt12Invoice as proper type instead of raw bytes
This patch refactors the BOLT12 invoice exposure in PaymentSuccessful to use properly typed wrappers instead of raw Vec<u8> bytes. Problem: The previous implementation exposed the BOLT12 invoice as Option<Vec<u8>> which required users to manually deserialize the bytes and didn't provide a good API experience. Additionally, UniFFI bindings need proper type wrappers to generate idiomatic foreign language bindings. Solution: - Add StaticInvoice wrapper type with accessor methods (offer_id, is_offer_expired, signing_pubkey, etc.) - Create PaidBolt12Invoice as a struct with a discriminant enum (PaidBolt12InvoiceKind) and optional fields for bolt12_invoice and static_invoice - Implement From<LdkPaidBolt12Invoice> for automatic conversion - Add Writeable/Readable implementations for serialization - Update UDL bindings with StaticInvoice interface and PaidBolt12Invoice dictionary Design decisions: - PaidBolt12Invoice is a struct (dictionary) rather than an enum because UniFFI does not support objects in enum variant data. The workaround uses a discriminant enum (PaidBolt12InvoiceKind) with optional object fields, which is the recommended pattern per UniFFI documentation. - Both uniffi and non-uniffi builds share the same API through conditional compilation in types.rs, ensuring consistent behavior across all build configurations. Signed-off-by: Vincenzo Palazzo <[email protected]>
1 parent ee08962 commit a3a60b3

File tree

6 files changed

+458
-24
lines changed

6 files changed

+458
-24
lines changed

bindings/ldk_node.udl

Lines changed: 28 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, sequence<u8>? bolt12_invoice);
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);
@@ -850,6 +850,33 @@ interface Bolt12Invoice {
850850
sequence<u8> encode();
851851
};
852852

853+
interface StaticInvoice {
854+
[Throws=NodeError, Name=from_str]
855+
constructor([ByRef] string invoice_str);
856+
OfferId offer_id();
857+
boolean is_offer_expired();
858+
PublicKey signing_pubkey();
859+
PublicKey? issuer_signing_pubkey();
860+
string? invoice_description();
861+
string? issuer();
862+
OfferAmount? amount();
863+
sequence<u8> chain();
864+
sequence<u8>? metadata();
865+
u64? absolute_expiry_seconds();
866+
sequence<u8> encode();
867+
};
868+
869+
enum PaidBolt12InvoiceKind {
870+
"Bolt12Invoice",
871+
"StaticInvoice",
872+
};
873+
874+
dictionary PaidBolt12Invoice {
875+
PaidBolt12InvoiceKind kind;
876+
Bolt12Invoice? bolt12_invoice;
877+
StaticInvoice? static_invoice;
878+
};
879+
853880
[Custom]
854881
typedef string Txid;
855882

src/event.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ use bitcoin::secp256k1::PublicKey;
1616
use bitcoin::{Amount, OutPoint};
1717
use lightning::events::bump_transaction::BumpTransactionEvent;
1818
use lightning::events::{
19-
ClosureReason, Event as LdkEvent, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose,
20-
ReplayEvent,
19+
ClosureReason, Event as LdkEvent, PaymentFailureReason, PaymentPurpose, ReplayEvent,
2120
};
2221
use lightning::impl_writeable_tlv_based_enum;
2322
use lightning::ln::channelmanager::PaymentId;
@@ -49,7 +48,9 @@ use crate::payment::store::{
4948
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
5049
};
5150
use crate::runtime::Runtime;
52-
use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet};
51+
use crate::types::{
52+
CustomTlvRecord, DynStore, OnionMessenger, PaidBolt12Invoice, PaymentStore, Sweeper, Wallet,
53+
};
5354
use crate::{
5455
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
5556
UserChannelId,
@@ -76,17 +77,17 @@ pub enum Event {
7677
payment_preimage: Option<PaymentPreimage>,
7778
/// The total fee which was spent at intermediate hops in this payment.
7879
fee_paid_msat: Option<u64>,
79-
/// The BOLT12 invoice that was paid, serialized as bytes.
80+
/// The BOLT12 invoice that was paid.
8081
///
8182
/// This is useful for proof of payment. A third party can verify that the payment was made
8283
/// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`.
8384
///
84-
/// Will be `None` for non-BOLT12 payments, or for async payments (`StaticInvoice`)
85-
/// where proof of payment is not possible.
85+
/// Will be `None` for non-BOLT12 payments.
8686
///
87-
/// To parse the invoice in native Rust, use `Bolt12Invoice::try_from(bytes)`.
88-
/// In FFI bindings, hex-encode the bytes and use `Bolt12Invoice.from_str(hex_string)`.
89-
bolt12_invoice: Option<Vec<u8>>,
87+
/// Note that static invoices (indicated by [`crate::payment::PaidBolt12InvoiceKind::StaticInvoice`],
88+
/// used for async payments) do not support proof of payment as the payment hash is not
89+
/// derived from a preimage known only to the recipient.
90+
bolt12_invoice: Option<PaidBolt12Invoice>,
9091
},
9192
/// A sent payment has failed.
9293
PaymentFailed {
@@ -1077,19 +1078,15 @@ where
10771078
);
10781079
});
10791080

1080-
// Serialize the BOLT12 invoice to bytes for proof of payment.
1081-
// Only Bolt12Invoice supports proof of payment; StaticInvoice does not.
1082-
let bolt12_invoice_bytes = bolt12_invoice.and_then(|inv| match inv {
1083-
PaidBolt12Invoice::Bolt12Invoice(invoice) => Some(invoice.encode()),
1084-
PaidBolt12Invoice::StaticInvoice(_) => None,
1085-
});
1081+
// Convert LDK's PaidBolt12Invoice to our wrapped type.
1082+
let bolt12_invoice_wrapped = bolt12_invoice.map(PaidBolt12Invoice::from);
10861083

10871084
let event = Event::PaymentSuccessful {
10881085
payment_id: Some(payment_id),
10891086
payment_hash,
10901087
payment_preimage: Some(payment_preimage),
10911088
fee_paid_msat,
1092-
bolt12_invoice: bolt12_invoice_bytes,
1089+
bolt12_invoice: bolt12_invoice_wrapped,
10931090
};
10941091

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

src/ffi/types.rs

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ 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
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3336
pub use lightning::routing::router::RouteParametersConfig;
34-
use lightning::util::ser::Writeable;
37+
use lightning::util::ser::{Readable, Writeable, Writer};
3538
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
3639
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
3740
pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -619,6 +622,205 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
619622
}
620623
}
621624

625+
/// A `StaticInvoice` is used for async payments where the recipient may be offline.
626+
///
627+
/// Unlike [`Bolt12Invoice`], a `StaticInvoice` does not support proof of payment
628+
/// because the payment hash is not derived from a preimage known only to the recipient.
629+
#[derive(Debug, Clone, PartialEq, Eq)]
630+
pub struct StaticInvoice {
631+
pub(crate) inner: LdkStaticInvoice,
632+
}
633+
634+
impl StaticInvoice {
635+
pub fn from_str(invoice_str: &str) -> Result<Self, Error> {
636+
invoice_str.parse()
637+
}
638+
639+
/// Returns the [`OfferId`] of the underlying [`Offer`] this invoice corresponds to.
640+
///
641+
/// [`Offer`]: lightning::offers::offer::Offer
642+
pub fn offer_id(&self) -> OfferId {
643+
OfferId(self.inner.offer_id().0)
644+
}
645+
646+
/// Whether the offer this invoice corresponds to has expired.
647+
pub fn is_offer_expired(&self) -> bool {
648+
self.inner.is_offer_expired()
649+
}
650+
651+
/// A typically transient public key corresponding to the key used to sign the invoice.
652+
pub fn signing_pubkey(&self) -> PublicKey {
653+
self.inner.signing_pubkey()
654+
}
655+
656+
/// The public key used by the recipient to sign invoices.
657+
pub fn issuer_signing_pubkey(&self) -> Option<PublicKey> {
658+
self.inner.issuer_signing_pubkey()
659+
}
660+
661+
/// A complete description of the purpose of the originating offer.
662+
pub fn invoice_description(&self) -> Option<String> {
663+
self.inner.description().map(|printable| printable.to_string())
664+
}
665+
666+
/// The issuer of the offer.
667+
pub fn issuer(&self) -> Option<String> {
668+
self.inner.issuer().map(|printable| printable.to_string())
669+
}
670+
671+
/// The minimum amount required for a successful payment of a single item.
672+
pub fn amount(&self) -> Option<OfferAmount> {
673+
self.inner.amount().map(|amount| amount.into())
674+
}
675+
676+
/// The chain that must be used when paying the invoice.
677+
pub fn chain(&self) -> Vec<u8> {
678+
self.inner.chain().to_bytes().to_vec()
679+
}
680+
681+
/// Opaque bytes set by the originating [`Offer`].
682+
///
683+
/// [`Offer`]: lightning::offers::offer::Offer
684+
pub fn metadata(&self) -> Option<Vec<u8>> {
685+
self.inner.metadata().cloned()
686+
}
687+
688+
/// Seconds since the Unix epoch when an invoice should no longer be requested.
689+
///
690+
/// If `None`, the offer does not expire.
691+
pub fn absolute_expiry_seconds(&self) -> Option<u64> {
692+
self.inner.absolute_expiry().map(|duration| duration.as_secs())
693+
}
694+
695+
/// Writes `self` out to a `Vec<u8>`.
696+
pub fn encode(&self) -> Vec<u8> {
697+
self.inner.encode()
698+
}
699+
}
700+
701+
impl std::str::FromStr for StaticInvoice {
702+
type Err = Error;
703+
704+
fn from_str(invoice_str: &str) -> Result<Self, Self::Err> {
705+
if let Some(bytes_vec) = hex_utils::to_vec(invoice_str) {
706+
if let Ok(invoice) = LdkStaticInvoice::try_from(bytes_vec) {
707+
return Ok(StaticInvoice { inner: invoice });
708+
}
709+
}
710+
Err(Error::InvalidInvoice)
711+
}
712+
}
713+
714+
impl From<LdkStaticInvoice> for StaticInvoice {
715+
fn from(invoice: LdkStaticInvoice) -> Self {
716+
StaticInvoice { inner: invoice }
717+
}
718+
}
719+
720+
impl Deref for StaticInvoice {
721+
type Target = LdkStaticInvoice;
722+
fn deref(&self) -> &Self::Target {
723+
&self.inner
724+
}
725+
}
726+
727+
impl AsRef<LdkStaticInvoice> for StaticInvoice {
728+
fn as_ref(&self) -> &LdkStaticInvoice {
729+
self.deref()
730+
}
731+
}
732+
733+
/// The kind of [`PaidBolt12Invoice`].
734+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735+
pub enum PaidBolt12InvoiceKind {
736+
/// A standard BOLT12 invoice.
737+
Bolt12Invoice,
738+
/// A static invoice for async payments.
739+
StaticInvoice,
740+
}
741+
742+
/// Represents a BOLT12 invoice that was paid.
743+
///
744+
/// This is used in [`Event::PaymentSuccessful`] to provide proof of payment for BOLT12 payments.
745+
///
746+
/// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful
747+
#[derive(Debug, Clone, PartialEq, Eq)]
748+
pub struct PaidBolt12Invoice {
749+
/// The kind of invoice that was paid.
750+
pub kind: PaidBolt12InvoiceKind,
751+
/// The paid BOLT12 invoice, if this is a regular BOLT12 invoice.
752+
pub bolt12_invoice: Option<Arc<Bolt12Invoice>>,
753+
/// The paid static invoice, if this is a static invoice (async payment).
754+
pub static_invoice: Option<Arc<StaticInvoice>>,
755+
}
756+
757+
impl From<LdkPaidBolt12Invoice> for PaidBolt12Invoice {
758+
fn from(ldk_invoice: LdkPaidBolt12Invoice) -> Self {
759+
match ldk_invoice {
760+
LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => PaidBolt12Invoice {
761+
kind: PaidBolt12InvoiceKind::Bolt12Invoice,
762+
bolt12_invoice: Some(Arc::new(Bolt12Invoice { inner: invoice })),
763+
static_invoice: None,
764+
},
765+
LdkPaidBolt12Invoice::StaticInvoice(invoice) => PaidBolt12Invoice {
766+
kind: PaidBolt12InvoiceKind::StaticInvoice,
767+
bolt12_invoice: None,
768+
static_invoice: Some(Arc::new(StaticInvoice { inner: invoice })),
769+
},
770+
}
771+
}
772+
}
773+
774+
impl Writeable for PaidBolt12Invoice {
775+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
776+
match self.kind {
777+
PaidBolt12InvoiceKind::Bolt12Invoice => {
778+
0u8.write(writer)?;
779+
if let Some(invoice) = &self.bolt12_invoice {
780+
let bytes = invoice.inner.encode();
781+
bytes.write(writer)?;
782+
}
783+
},
784+
PaidBolt12InvoiceKind::StaticInvoice => {
785+
1u8.write(writer)?;
786+
if let Some(invoice) = &self.static_invoice {
787+
let bytes = invoice.inner.encode();
788+
bytes.write(writer)?;
789+
}
790+
},
791+
}
792+
Ok(())
793+
}
794+
}
795+
796+
impl Readable for PaidBolt12Invoice {
797+
fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
798+
let tag: u8 = Readable::read(reader)?;
799+
let bytes: Vec<u8> = Readable::read(reader)?;
800+
match tag {
801+
0 => {
802+
let invoice =
803+
LdkBolt12Invoice::try_from(bytes).map_err(|_| DecodeError::InvalidValue)?;
804+
Ok(PaidBolt12Invoice {
805+
kind: PaidBolt12InvoiceKind::Bolt12Invoice,
806+
bolt12_invoice: Some(Arc::new(Bolt12Invoice { inner: invoice })),
807+
static_invoice: None,
808+
})
809+
},
810+
1 => {
811+
let invoice =
812+
LdkStaticInvoice::try_from(bytes).map_err(|_| DecodeError::InvalidValue)?;
813+
Ok(PaidBolt12Invoice {
814+
kind: PaidBolt12InvoiceKind::StaticInvoice,
815+
bolt12_invoice: None,
816+
static_invoice: Some(Arc::new(StaticInvoice { inner: invoice })),
817+
})
818+
},
819+
_ => Err(DecodeError::InvalidValue),
820+
}
821+
}
822+
}
823+
622824
impl UniffiCustomTypeConverter for OfferId {
623825
type Builtin = String;
624826

src/payment/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ pub use store::{
2323
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
2424
};
2525
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};
26+
27+
pub use crate::types::{Bolt12Invoice, PaidBolt12Invoice, PaidBolt12InvoiceKind, StaticInvoice};

0 commit comments

Comments
 (0)