Add UniFFI Java bindings for payjoin-ffi - #1882
Open
ram0verflow wants to merge 5 commits into
Open
ram0verflow wants to merge 5 commits into
ram0verflow wants to merge 5 commits into
Conversation
Add a [bindings.java] section to uniffi.toml so uniffi-bindgen-java can find its package name and cdylib name the same way the existing Kotlin/Python sections already do. The close/closeSession rename mirrors [bindings.kotlin.rename] exactly, for the same reason: AutoCloseable.close() would otherwise collide with the protocol's own close() on these types. Keeping the two rename tables identical also keeps the Java and Kotlin binding APIs shaped the same way for the same underlying types.
Gradle project scaffolding for the Java target: build.gradle.kts, settings.gradle.kts, gradle.properties, the wrapper, and scripts/generate_bindings.sh, which (re)generates src/main/java/ and lib/ before every build, the same way the Kotlin/Python targets' own scripts work. No generated sources are committed. Generated bindings use Java's Foreign Function & Memory API (java.lang.foreign, finalized in JDK 22 - JEP 454) rather than JNA, so this target has zero runtime dependencies. build.gradle.kts uses `--release 22` rather than sourceCompatibility/targetCompatibility so a build running under a newer JDK still fails loudly if it accidentally depends on a post-22 API a real JDK 22 consumer wouldn't have. payjoin-ffi's UniFFI 0.31.x metadata needs uniffi-bindgen-java's still-unreleased 0.5.0 fixes for its Java error-type templates (see IronCoreLabs/uniffi-bindgen-java#68); the latest tagged release, 0.4.2, emits Java that doesn't compile for this crate. Rather than depend on anyone's fork or wait on that release, generate_bindings.sh builds patched-canonical-source instead: the exact upstream commit the 0.4.2 tag points to, with the fix backported as the small, git-apply'able patch under patches/. Kotlin generates through uniffi's own official, in-tree CLI (payjoin-ffi's own uniffi-bindgen binary target, sharing the workspace's build cache); uniffi-bindgen-java is a third-party crate with no such official Java backend to call into instead, so this target has to build its own generator from source, cached by pinned-commit and patch hash so that cost is paid once, not on every run. The patch is generated with `git diff -U0` (zero context lines) and applied with `git apply --unidiff-zero`: since it's always applied against this one pinned commit, dropping context is safe and keeps upstream's own incidental whitespace in the surrounding template text out of the patch file. See README.md's "Generator provenance" section for the full reasoning, the exact pinned commit, and the migration plan once payjoin-ffi moves to UniFFI 0.32.
A focused Java port of the Kotlin bindings' unit test suites: URI parsing, sender/receiver builder validation, session persistence, and session cancellation - not a mechanical line-for-line port of every existing test. Validation tests for InputPair assert the exact nested exception variant (e.g. InputPairException.InvalidOutPoint vs. FfiValidationException.AmountOutOfRange), not just the shared superclass: a too-long txid and a valid txid with an out-of-range amount both throw InputPairException, but for different underlying reasons, and only the exact variant tells them apart. Cancellation is driven through the real protocol transitions (cancel() -> save() -> ... -> closeSession() -> save()) rather than by calling the persister's closeSession() helper directly, so the tests actually prove Rust invokes it, not just that the helper itself works. The receiver and sender paths turn out to be asymmetric: the sender always already holds a signed original PSBT and so always gets a real pending-fallback object back, while a receiver that has never received anything has no fallback and closes immediately from cancel().save() itself - both are asserted directly rather than assumed. PersistenceFailureTest and the async half of AsyncPersistenceTest extend this to storage failures and the CompletableFuture-based async persister API: a failure on a specific persister operation (save/load/close) propagates as a real, specifically-typed exception, and a gated persister proves the returned future genuinely stays pending until the persister's own future resolves, not fire-and-forget.
A close Java port of the Kotlin bindings' IntegrationTests.kt: a full BIP 77 v2<->v2 round trip against payjoin-test-utils' in-process directory, OHTTP relay, and regtest bitcoind, receiver and sender both driven through the generated Java API, ending in a broadcast transaction that spends coins from both wallets. No public production infrastructure. TestHttp is a near-direct port of the Kotlin equivalent. JsonRpc is a small hand-rolled recursive-descent parser for bitcoind's RPC responses, since the JDK has no built-in JSON parser and adding a JSON library dependency isn't warranted for this test's simple, fixed response shapes. Every generated type here genuinely implements AutoCloseable, so resource cleanup uses plain try-with-resources throughout (unlike the Kotlin bindings, where some types are only Disposable). The three RPC-backed callbacks (mempool acceptance, input/script ownership) only return a legitimate false for a cleanly-parsed negative answer from bitcoind; an RPC or transport failure instead throws, so a broken check fails the test instead of silently reading as a negative answer.
flake.nix: a java devShell alongside the existing kotlin one, adding what generated Java needs beyond it - jdk25 (the Foreign Function & Memory API is only finalized from JDK 22 onward) and Rust new enough for both payjoin-ffi's own MSRV and the pinned generator's slightly newer one, deliberately one toolchain rather than two on the same PATH. A rust-toolchain.toml file has no effect inside a nix devShell (there is no rustup here for it to redirect), which is why this needed a real toolchain choice rather than relying on one. .github/workflows/java.yml mirrors kotlin.yml's structure exactly: build and test through the nix devShell on Linux and macOS, plus a separate step compiling production bindings (PAYJOIN_FFI_FEATURES unset to empty) to catch anything that only the test-utils feature set was masking. This matrix targets Linux and macOS, not a claim that either has actually run this yet - see payjoin-ffi/java/ README.md's "Platforms" section for what has and hasn't actually been verified where.
ram0verflow
requested review from
DanGould,
benalleng and
spacebear21
as code owners
September 15, 2026 09:48
Collaborator
Coverage Report for CI Build 34954587323Coverage remained the same at 86.646%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #1860.
Adds generated Java bindings for
payjoin-ffiunderpayjoin-ffi/java.The bindings are generated with
uniffi-bindgen-javaand use Java's Foreign Function & Memory API, requiring JDK 22+ without adding a Java runtime dependency such as JNA.Generator
payjoin-fficurrently uses UniFFI 0.31.x.uniffi-bindgen-java0.4.2 can read that metadata, but its error templates emit invalid Java for severalpayjoin-ffitypes. The relevant fixes already exist on the newer upstream generator line, which requires UniFFI 0.32.For now, generation uses canonical
IronCoreLabs/uniffi-bindgen-javaat the exact commit referenced by its signed0.4.2tag:559bd72e680e0be7feda6ac3a93819376db030d9A small patch kept in
payjoin-ffi/java/patches/backports the required template fixes: 13 insertions / 5 deletions acrossErrorTemplate.javaandmacros.java.The generator source is not vendored and there is no dependency on a contributor-owned fork. The generation script fetches the pinned upstream commit into a temporary checkout, applies the patch, builds the generator, and discards the checkout.
IronCoreLabs/uniffi-bindgen-java#68 tracks the 0.31-compatible generator issue.
When
payjoin-ffieventually moves to UniFFI 0.32, the intention is to replace this temporary patched-generator path with an appropriate upstream release and validate that migration by regenerating and running this Java test suite.Generated Java sources and native libraries are not committed.
Tests
The Java target includes coverage for:
closeSessionCompletableFutureExecutordispatchIt also includes a full local BIP77 v2↔v2 integration test using the existing
payjoin-test-utilsinfrastructure:Java sender → local OHTTP relay/directory → Java receiver → proposal → sender finalize/broadcast → regtest
bitcoindThe test asserts that the resulting transaction spends inputs from both sender and receiver wallets.
Locally on macOS/aarch64:
_test-utilspassesPAYJOIN_FFI_FEATURES=produces and compiles the production API--release 22CI targets Linux and macOS. Windows is currently unvalidated for this Java target.
Scope
This PR does not include:
Those can be handled separately once the Java binding target itself is established.
AI assistance
Claude Code (Opus5) substantially assisted with implementation, tests, documentation, and branch preparation. ChatGPT (GPT-6-Astra) assisted with architecture review, reviewing the branch against the Kotlin binding review history, and drafting this PR description. I reviewed the resulting changes and exercised the generated bindings and BIP77 integration locally.
Pull Request Checklist