Skip to content

ldk-server-client: Add uniffi bindings#194

Draft
tnull wants to merge 7 commits intolightningdevkit:mainfrom
tnull:2026-04-uniffi-bindings
Draft

ldk-server-client: Add uniffi bindings#194
tnull wants to merge 7 commits intolightningdevkit:mainfrom
tnull:2026-04-uniffi-bindings

Conversation

@tnull
Copy link
Copy Markdown
Collaborator

@tnull tnull commented Apr 17, 2026

Not quite sure we want to add this at this point, but I needed it for a personal project, so vibecoded it. Draft for now.

tnull added 7 commits April 17, 2026 12:55
Adds opt-in `uniffi` and `uniffi-cli` features so foreign-language consumers
(starting with Kotlin for an upcoming Android remote-control app) can call
this crate directly instead of reimplementing HMAC-SHA256 auth and gRPC
framing.

Uses UniFFI's single-crate pattern from the official guide: the bindgen CLI
is a [[bin]] target inside this crate, gated on the `uniffi-cli` feature,
avoiding the complexity of a separate workspace crate. Wrapper types and
the client object are added in follow-up commits.

Co-Authored-By: HAL 9000
Defines flat, Kotlin/Swift-friendly analogues for every prost type that the
upcoming FFI-exposed client methods will surface: node info, balances,
payments (with a flattened `PaymentKindInfo` collapsing the prost `oneof`),
channels, peers, send/receive results, decoded invoices and offers, and
pagination tokens.

Prost types can't be exported directly — they carry `#[derive(::prost::Message)]`,
use nested `oneof` submodules, and reach for `prost::bytes::Bytes`. Each
wrapper comes with the `From<ProstType>` (and, where both directions apply,
`Into<ProstType>`) conversions the exported client will use.

Detail deliberately dropped for the MVP scope:
- `lightning_balances` and `pending_balances_from_channel_closures` (deep oneof nesting)
- `OfferAmount::CurrencyAmount` — flatten to `amount_msat: Option<u64>`
- `features` maps and `route_hints` on decoded invoice/offer
- Byte-typed `secret` fields on payment kinds

Co-Authored-By: HAL 9000
Adds the UniFFI-exported `LdkServerClientUni` object and its async
constructor + read-only query methods: get_node_info, get_balances,
list_channels, list_peers, list_payments (with optional page_token),
get_payment_details.

Why a wrapper rather than exporting LdkServerClient directly: the inner
reqwest/hyper clients aren't UniFFI-exportable types, but the whole client
is `Clone + Send + Sync`, so wrapping it in a `uniffi::Object` satisfies
both UniFFI's trait bounds and the Kotlin/Swift `suspend fun` async model
(via `async_runtime = "tokio"`).

The uniffi dep now enables its `tokio` feature so the generated FFI
scaffolding picks up the tokio-aware `#[uniffi::export]` expansion.

Co-Authored-By: HAL 9000
Rounds out `LdkServerClientUni` with the full set of methods a wallet UI
needs: the unified + per-protocol send flows (unified_send, bolt11_send,
bolt12_send, onchain_send), receive flows (bolt11_receive, bolt12_receive,
onchain_receive), channel lifecycle (open/close/force-close), peer
connect/disconnect, and invoice/offer decoding.

UniFFI-side API surface is intentionally simpler than the underlying
prost requests: the `route_parameters` configuration on send methods and
the `channel_config` on open_channel are hidden (server defaults are what
we want); BOLT11 `description` is exposed as `Option<String>` and always
attached as a direct description — the description-hash variant isn't
useful to a mobile wallet.

Co-Authored-By: HAL 9000
Covers the From/Into conversions for every wrapper type: node info
(with + without a best block), balances, all PaymentKind oneof variants,
channels, peers, all UnifiedSendResult variants (including the
protocol-violation error path when the oneof is empty), decoded invoices,
and decoded offers (including the flatten-to-None behavior for
currency-denominated amounts).

Also pins down the graceful-degradation guarantee for unknown protobuf
enum values: if the server returns a direction/status code the client
doesn't recognize, we fall back to safe defaults (Outbound/Pending)
rather than panicking.

Co-Authored-By: HAL 9000
Verified end-to-end:
  * cross-compile produces ~3.3-4.7 MB libldk_server_client.so per ABI
  * generated ldk_server_client.kt has `suspend fun` stubs for every
    exported method and data classes/enums for every wrapper type
  * jniLibs folder layout matches what the Android Gradle plugin picks up.

Co-Authored-By: HAL 9000
UniFFI's Kotlin generator emits struct-variant errors as subclasses of
Throwable, and a constructor property named `message` collides with
Throwable.message, producing a compile error on the generated
ldk_server_client.kt. Renaming the field to `reason` sidesteps the clash
without changing the FFI surface meaningfully.

No behavior change; Display output now reads "Invalid request: <reason>"
etc. All existing unit tests still pass.

Co-Authored-By: HAL 9000
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 17, 2026

👋 Hi! This PR is now in draft status.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@tnull tnull marked this pull request as draft April 17, 2026 13:11
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.

2 participants