Skip to content

Feature: Add Bolt12 Uniffi Type Wrappers #542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

alexanderwiederin
Copy link
Contributor

@alexanderwiederin alexanderwiederin commented May 19, 2025

Second PR for #504.

This PR adds full Offer-, Refund- and Bolt12Invoice- types for the FFI bindings.

Changes:

  • Type definitions in ldk_node.udl and uniffi_types.rs
  • Implementation of UniffiType trait to standardize conversions between LDK types and their FFI-compatible wrappers in uniffi_conversions.rs
  • Integration of wrapper types in payment handling methods in bolt12.rs and unified_qr.rs

Benefits:

  • Direct access to Offer, Refund and Bolt12Invoice properties from FFI languages
  • Consistent API across languages

Note:

Appreciate that this PR is big. Happy to break down into smaller parts if preferred.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented May 19, 2025

👋 Thanks for assigning @enigbe as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

match LdkBolt11InvoiceDescription::try_from(self) {
Ok(ldk_description) => ldk_description,
Err(e) => {
panic!("Failed to convert Bolt11InvoiceDescription to LDK type: {:?}", e)
Copy link
Contributor Author

@alexanderwiederin alexanderwiederin May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of this myself. We could refactor the Bolt11InvoiceDescription to be a struct that holds the LdkType in an inner field (similar to what we did to other types) to make this conversion safer. Looking for advice on whether it's worth introducing a breaking change for this!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, indeed, if we're going to unify the conversions, it would make sense to align LdkBolt11InvoiceDescription. Generally, we're still on 0.X, so don't care about breaking API all too much. It's much more important to add features and figure out the best API over time at this point.

But do you think LdkBolt11InvoiceDescription would even work as expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would work differently. I was thinking something like this:

pub struct Bolt11InvoiceDescription {
	inner: lightning_invoice::Bolt11InvoiceDescription,
}

impl Bolt11InvoiceDescription {
	pub fn direct_description(&self) -> Option<String> {
		match &self.inner {
			lightning_invoice::Bolt11InvoiceDescription::Direct(description) => {
				Some(description.to_string())
			},
			_ => None,
		}
	}

	pub fn hash(&self) -> Option<String> {
		match &self.inner {
			lightning_invoice::Bolt11InvoiceDescription::Hash(hash) => {
				Some(hex_utils::to_string(hash.0.as_ref()))
			},
			_ => None,
		}
	}
}

Not ideal, because as you say it works differently, but safe. What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh, but is something stopping us from splitting this up in UnffiConversionType and UniffiFallibleConversionType, where the latter would have a maybe_try_convert(value: &T, error: Error) method that would return and bubble up that error if conversion fails?

FWIW, we could even have them inherit all the other behavior from UniffiType, i.e.

trait UniffiConversionType: UniffiType or similar, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deviated from your suggestion, but think I have something neat - let me know what you think! Thanks for the pointer.

@alexanderwiederin alexanderwiederin marked this pull request as ready for review May 20, 2025 21:21
@tnull tnull requested review from tnull and removed request for valentinewallace May 22, 2025 15:13
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, already looks pretty good! Did a first higher-level pass.

match LdkBolt11InvoiceDescription::try_from(self) {
Ok(ldk_description) => ldk_description,
Err(e) => {
panic!("Failed to convert Bolt11InvoiceDescription to LDK type: {:?}", e)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, indeed, if we're going to unify the conversions, it would make sense to align LdkBolt11InvoiceDescription. Generally, we're still on 0.X, so don't care about breaking API all too much. It's much more important to add features and figure out the best API over time at this point.

But do you think LdkBolt11InvoiceDescription would even work as expected?

}
}

impl std::str::FromStr for Bolt12Invoice {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think we should wait for the resolution of the discussion over at lightningdevkit/rust-lightning#3800 before we do this and roll our own custom serialization.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, AFAICT the resolution was to use the wire format for now. I'm not super happy about that, but okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmh, so what does this mean for us? We need to implement Writable and Readable for Bolt12Invoice?

match LdkBolt11InvoiceDescription::try_from(self) {
Ok(ldk_description) => ldk_description,
Err(e) => {
panic!("Failed to convert Bolt11InvoiceDescription to LDK type: {:?}", e)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh, but is something stopping us from splitting this up in UnffiConversionType and UniffiFallibleConversionType, where the latter would have a maybe_try_convert(value: &T, error: Error) method that would return and bubble up that error if conversion fails?

FWIW, we could even have them inherit all the other behavior from UniffiType, i.e.

trait UniffiConversionType: UniffiType or similar, no?

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I like the proposed approach. Very clean!

Just a few comments.

@@ -50,6 +51,20 @@ mod uniffi_impl {
}
}

impl UniffiType for Arc<Offer> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we implementing this on Arc<Offer>, not on Offer? I think we should do the latter if possible.

Copy link
Contributor Author

@alexanderwiederin alexanderwiederin Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

Initially, I implemented these on the Arcs because that's how they are required to pass the ffi boundary. I looked into it again and now have a solution that Arc-allocates/-extracts the types in the conversion functions (i.e. maybe_convert, maybe_wrap etc.) and only does type conversion on the implementation methods. The benefit is that we can use standard traits and remove ffi/conversions.rs entirely. Downside is that the conversion functions become slightly more complex.

I have implemented these changes on alexanderwiederin@6b5f297 for better comparison.

What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a lot less invasive. However, there seem to be a few cases where we add unnecessary clones now that could lead to addtional memory usage and heap fragmentation. Can we avoid cloning when not necessary, and also enforce in the types of maybe_convert etc, that we'd always use Arc::clone(&..), ie., that we'd only ever clone the reference, and otherwise would maintain noops or move semantics?

@@ -65,6 +65,20 @@ mod uniffi_impl {
}
}

impl UniffiType for Arc<Refund> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, can we implement this rather for Refund directly?

This commit reorganizes the FFI architecture by introducing conversion
traits for lightning types. Moves code from uniffi_types.rs to a
dedicated ffi module for separation of concerns.
Implement Offer struct in ffi/types.rs to provide a wrapper around LDK's Offer for cross-language bindings.

Modified payment handling in bolt12.rs to:
- Support both native and FFI-compatible types via type aliasing
- Implement conditional compilation for transparent FFI support
- Update payment functions to handle wrapped types

Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types.
Implement Refund struct in ffi/types.rs to provide a wrapper around LDK's Refund for cross-language bindings.

Modified payment handling in bolt12.rs to:
- Support both native and FFI-compatible types via type aliasing
- Implement conditional compilation for transparent FFI support
- Update payment functions to handle wrapped types

Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types.
Implement Bolt12Invoice struct in ffi/types.rs to provide a wrapper around LDK's Bolt12Invoice for cross-language bindings.

Modified payment handling in bolt12.rs to:
- Support both native and FFI-compatible types via type aliasing
- Implement conditional compilation for transparent FFI support
- Update payment functions to handle wrapped types

Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types.
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @enigbe! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants