Skip to content

Commit 78d71dc

Browse files
event: conditionally expose bolt12_invoice for non-UniFFI builds only
Due to UniFFI 0.28 limitations (Object types in enum variants are not supported), the `bolt12_invoice` field in `PaymentSuccessful` cannot be exposed in bindings until we upgrade to UniFFI 0.29+. Problem: The previous commit added `PaidBolt12Invoice` to the `PaymentSuccessful` event, but UniFFI 0.28 cannot handle Object types (like `Bolt12Invoice`) inside enum variant data, causing binding generation to fail. Solution: Use `#[cfg(not(feature = "uniffi"))]` to conditionally compile the `bolt12_invoice` field only for non-UniFFI builds. This requires duplicating the `impl_writeable_tlv_based_enum!` macro invocation for Event, but the serialization formats remain compatible: TLV tag 7 written by non-UniFFI builds is silently skipped by UniFFI builds (unknown optional TLV), and vice versa. Changes: - Add `#[cfg]` guards to `bolt12_invoice` field in `PaymentSuccessful` - Duplicate Event serialization macro for UniFFI/non-UniFFI builds - Remove unused `StaticInvoice` and `PaidBolt12Invoice` FFI wrappers - Add consistent TODO comments referencing #757 for tracking - Update tests to handle both build configurations
1 parent a8d05cb commit 78d71dc

File tree

6 files changed

+150
-230
lines changed

6 files changed

+150
-230
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,8 @@ enum VssHeaderProviderError {
400400

401401
[Enum]
402402
interface Event {
403-
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat, PaidBolt12Invoice? bolt12_invoice);
403+
// TODO: Add bolt12_invoice field once we upgrade to UniFFI 0.29+. See #757.
404+
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat);
404405
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
405406
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
406407
PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence<CustomTlvRecord> custom_records);
@@ -857,29 +858,7 @@ interface Bolt12Invoice {
857858
sequence<u8> encode();
858859
};
859860

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-
};
861+
// TODO: Add StaticInvoice and PaidBolt12Invoice once we upgrade to UniFFI 0.29+. See #757.
883862

884863
[Custom]
885864
typedef string Txid;

src/event.rs

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ use crate::payment::store::{
4848
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
4949
};
5050
use crate::runtime::Runtime;
51-
use crate::types::{
52-
CustomTlvRecord, DynStore, OnionMessenger, PaidBolt12Invoice, PaymentStore, Sweeper, Wallet,
53-
};
51+
#[cfg(not(feature = "uniffi"))]
52+
use crate::types::PaidBolt12Invoice;
53+
use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet};
5454
use crate::{
5555
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
5656
UserChannelId,
@@ -87,6 +87,11 @@ pub enum Event {
8787
/// Note that static invoices (indicated by [`PaidBolt12Invoice::StaticInvoice`], used for
8888
/// async payments) do not support proof of payment as the payment hash is not derived
8989
/// from a preimage known only to the recipient.
90+
///
91+
/// This field is only available in non-UniFFI builds. See the module documentation for
92+
/// more information.
93+
// TODO: Expose in bindings once we upgrade to UniFFI 0.29+. See #757.
94+
#[cfg(not(feature = "uniffi"))]
9095
bolt12_invoice: Option<PaidBolt12Invoice>,
9196
},
9297
/// A sent payment has failed.
@@ -271,6 +276,17 @@ pub enum Event {
271276
},
272277
}
273278

279+
// TODO: These two macros are duplicated because the `Event::PaymentSuccessful` variant has a
280+
// different set of fields depending on the `uniffi` feature flag. The `bolt12_invoice` field
281+
// only exists in non-UniFFI builds, and the macro generates code that references struct fields
282+
// by name, so we can't use a single macro for both. The duplication can be removed once we
283+
// upgrade to UniFFI 0.29+, which supports Object types in enum variants.
284+
// See https://github.com/lightningdevkit/ldk-node/issues/757
285+
//
286+
// Note: The serialization formats are compatible - TLV tag 7 (bolt12_invoice) written by
287+
// non-UniFFI builds is silently skipped by UniFFI builds (unknown optional TLV), and UniFFI
288+
// builds write without tag 7, which non-UniFFI builds read as `bolt12_invoice: None`.
289+
#[cfg(not(feature = "uniffi"))]
274290
impl_writeable_tlv_based_enum!(Event,
275291
(0, PaymentSuccessful) => {
276292
(0, payment_hash, required),
@@ -342,6 +358,78 @@ impl_writeable_tlv_based_enum!(Event,
342358
},
343359
);
344360

361+
// UniFFI version of the macro - see the comment above for why this duplication exists.
362+
#[cfg(feature = "uniffi")]
363+
impl_writeable_tlv_based_enum!(Event,
364+
(0, PaymentSuccessful) => {
365+
(0, payment_hash, required),
366+
(1, fee_paid_msat, option),
367+
(3, payment_id, option),
368+
(5, payment_preimage, option),
369+
},
370+
(1, PaymentFailed) => {
371+
(0, payment_hash, option),
372+
(1, reason, upgradable_option),
373+
(3, payment_id, option),
374+
},
375+
(2, PaymentReceived) => {
376+
(0, payment_hash, required),
377+
(1, payment_id, option),
378+
(2, amount_msat, required),
379+
(3, custom_records, optional_vec),
380+
},
381+
(3, ChannelReady) => {
382+
(0, channel_id, required),
383+
(1, counterparty_node_id, option),
384+
(2, user_channel_id, required),
385+
(3, funding_txo, option),
386+
},
387+
(4, ChannelPending) => {
388+
(0, channel_id, required),
389+
(2, user_channel_id, required),
390+
(4, former_temporary_channel_id, required),
391+
(6, counterparty_node_id, required),
392+
(8, funding_txo, required),
393+
},
394+
(5, ChannelClosed) => {
395+
(0, channel_id, required),
396+
(1, counterparty_node_id, option),
397+
(2, user_channel_id, required),
398+
(3, reason, upgradable_option),
399+
},
400+
(6, PaymentClaimable) => {
401+
(0, payment_hash, required),
402+
(2, payment_id, required),
403+
(4, claimable_amount_msat, required),
404+
(6, claim_deadline, option),
405+
(7, custom_records, optional_vec),
406+
},
407+
(7, PaymentForwarded) => {
408+
(0, prev_channel_id, required),
409+
(1, prev_node_id, option),
410+
(2, next_channel_id, required),
411+
(3, next_node_id, option),
412+
(4, prev_user_channel_id, option),
413+
(6, next_user_channel_id, option),
414+
(8, total_fee_earned_msat, option),
415+
(10, skimmed_fee_msat, option),
416+
(12, claim_from_onchain_tx, required),
417+
(14, outbound_amount_forwarded_msat, option),
418+
},
419+
(8, SplicePending) => {
420+
(1, channel_id, required),
421+
(3, counterparty_node_id, required),
422+
(5, user_channel_id, required),
423+
(7, new_funding_txo, required),
424+
},
425+
(9, SpliceFailed) => {
426+
(1, channel_id, required),
427+
(3, counterparty_node_id, required),
428+
(5, user_channel_id, required),
429+
(7, abandoned_funding_txo, option),
430+
},
431+
);
432+
345433
pub struct EventQueue<L: Deref>
346434
where
347435
L::Target: LdkLogger,
@@ -1078,11 +1166,7 @@ where
10781166
);
10791167
});
10801168

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-
1169+
#[cfg(not(feature = "uniffi"))]
10861170
let event = Event::PaymentSuccessful {
10871171
payment_id: Some(payment_id),
10881172
payment_hash,
@@ -1091,6 +1175,18 @@ where
10911175
bolt12_invoice,
10921176
};
10931177

1178+
#[cfg(feature = "uniffi")]
1179+
let event = {
1180+
// bolt12_invoice not exposed in uniffi builds until UniFFI 0.29+
1181+
let _ = bolt12_invoice;
1182+
Event::PaymentSuccessful {
1183+
payment_id: Some(payment_id),
1184+
payment_hash,
1185+
payment_preimage: Some(payment_preimage),
1186+
fee_paid_msat,
1187+
}
1188+
};
1189+
10941190
match self.event_queue.add_event(event).await {
10951191
Ok(_) => return Ok(()),
10961192
Err(e) => {

src/ffi/types.rs

Lines changed: 1 addition & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,17 @@ 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;
2625
pub use lightning::events::{ClosureReason, PaymentFailureReason};
2726
use lightning::ln::channelmanager::PaymentId;
28-
use lightning::ln::msgs::DecodeError;
2927
pub use lightning::ln::types::ChannelId;
3028
use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice;
3129
pub use lightning::offers::offer::OfferId;
3230
use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer};
3331
use lightning::offers::refund::Refund as LdkRefund;
34-
use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice;
3532
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
3633
pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees};
3734
pub use lightning::routing::router::RouteParametersConfig;
38-
use lightning::util::ser::{Readable, Writeable, Writer};
35+
use lightning::util::ser::Writeable;
3936
use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef};
4037
pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
4138
pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
@@ -689,172 +686,6 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
689686
}
690687
}
691688

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-
858689
impl UniffiCustomTypeConverter for OfferId {
859690
type Builtin = String;
860691

src/payment/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,5 @@ pub use store::{
2424
};
2525
pub use unified::{UnifiedPayment, UnifiedPaymentResult};
2626

27+
#[cfg(not(feature = "uniffi"))]
2728
pub use crate::types::PaidBolt12Invoice;
28-
#[cfg(feature = "uniffi")]
29-
pub use crate::types::{Bolt12Invoice, StaticInvoice};

0 commit comments

Comments
 (0)