Skip to content

feat(android): init android SDK and expand uniffi coverage (T15) - #388

Open
MrImmortal09 wants to merge 6 commits into
mainfrom
om/android-sdk
Open

MrImmortal09 wants to merge 6 commits into
mainfrom
om/android-sdk

Conversation

@MrImmortal09

@MrImmortal09 MrImmortal09 commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Adds an Android SDK: the android/ Gradle project whose Kotlin bindings are generated from rust/fedimint-sdk's uniffi feature, the cacheable Nix cross-compile that produces its native libraries, the CI that builds the native library once and generates bindings from that artifact, and a demo app that drives every exported call from one screen.

Base branch note: this PR was originally stacked on om/ffi-stage1. That branch's own commits (T4, T7, T11, T12) have since landed on main individually, so the branch is now rebased directly onto current main — no merge commits, every commit here is this PR's own.

Details

There is no hand-written Kotlin. The #[uniffi::export] blocks in rust/fedimint-sdk hand out that crate's own types (Sdk, Federation, Ecash, Lightning, Onchain, AnyOperation, Mnemonic, InviteCode, Notes, and the rest), so this SDK is a view of that API rather than a copy that could drift from it. The surface now covers the whole per-federation API: federation lifecycle (status, reopen, close, forget, shutdown), the ecash/lightning/onchain facades' quotesend/receive calls, operation lookup and typed downcasts, seed recovery, activity history and metadata — not just the three Sdk bootstrap methods (createFedimintSdk, preview, join) the feature started with.

Two pieces of shared machinery live in the new ffi.rs:

  • ffi_operation! monomorphises one UniFFI object per concrete Operation<S> instantiation, since the real type is generic and a UniFFI object cannot be. Invoked once per facade, next to the state type it wraps.
  • QuoteClaim is the single-use guard every quote type (EcashQuote, LnQuote, OnchainQuote) carries. In plain Rust, send takes its quote by value, so a second attempt with the same quote is a compile error; a Kotlin/Swift binding only ever holds a shared Arc, so nothing else stops it from calling send twice. QuoteClaim turns that into a runtime QuoteExpired (already_executed: true) on the second attempt, shared across all three facades instead of copied three times.

Notes (ecash) crosses as a UniFFI object, the same way InviteCode and Mnemonic already do, rather than a bare String. Ecash notes are a bearer instrument — printing one hands over spendable value — and a generated Kotlin/Swift record prints every field through its automatic toString(). As an object, the token comes out only through an explicit display(). The two Rust records that hold notes by value (EcashSendDetails, EcashReceiveDetails) keep their plain-Rust shape and get an Arc<Notes>-holding projection exported under the same name, built through an exhaustively-destructured From impl — no .. — so a field added to the Rust record fails to compile here until the projection is updated too.

Building it is still the same two Nix-then-bindgen steps as before, with the driving script now delegating to the same two scripts CI runs (nix-build-android-so.sh, generate-kotlin-bindings.sh) instead of a separate copy of that logic, so just build-kotlin cannot silently diverge from what android-native.yaml / kotlin-sdk.yaml do.

The demo app (android/app) now has one section per SDK call — wallet status with a live balance, join (preview/join/recover), generate and pay a Lightning invoice, send and redeem ecash, parse an invite code, generate a deposit address, send on-chain, and recent activity — mirroring the layout of js/examples/vite-core's own demo, so every exported call has somewhere to be exercised by hand against a real federation.

Reviewing

The two places worth being opinionated about, carried over from the original review:

  • Where the Nix/bindgen split falls. Nix stops at the .so; bindings are generated outside it, so each generator is a cheap step over a shared artifact rather than a second derivation re-entering the cross-compile.
  • Retiring the RN lanes rather than disabling them, since uniffi-bindgen-react-native is pinned to uniffi 0.31 and cannot read this crate's 0.32 metadata. The React Native packages under js/ are untouched.

New in this round:

  • The single-use quote guard (QuoteClaim) sits at the FFI boundary rather than in the plain-Rust send methods, since the by-value signature already enforces it there. Worth checking that the three call sites (ecash/lightning/onchain ffi_send) all claim before calling into the real send, not after.
  • Notes as an object vs. a uniffi::custom_type! over String. The custom-type form is a few lines shorter but the token would still cross as a plain string, which a generated data class's toString() would print. Went with the object for parity with InviteCode/Mnemonic.
  • The hand-copied Ffi* detail records for ecash, whose only alternative was making the details themselves UniFFI objects (one accessor method per field, and inconsistent with every other facade's plain records).

Testing

Verified locally on macOS (nix develop .#android):

  • cargo fmt --check, cargo clippy --locked --tests -- -D warnings with and without --features uniffi, and cargo doc --no-deps --features uniffi with warnings as errors — all clean.
  • cargo test --lib — 529 passing, with and without --features uniffi.
  • ./scripts/build-android-sdk.sh — full Nix cross-compile (arm64-v8a + x86_64) and Kotlin generation.
  • :app:installDebug onto a Pixel emulator, then exercised by hand against a live mutinynet federation (fed11qgq... — the same testnet federation js/examples/vite-core pre-fills): open wallet (loads/generates the seed, reattaches to an already-joined federation), preview, join (including the already-joined → reattach path), generate a Lightning invoice, and generate a deposit address, all completed successfully with live-updating operation state.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: bf8cc00

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@socket-security

socket-security Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmaven/​junit/​junit@​4.13.236100907570
Addedmaven/​net.java.dev.jna/​jna@​5.17.0361009010070
Addedmaven/​org.jetbrains.kotlinx/​kotlinx-coroutines-android@​1.8.19810090100100
Addedmaven/​org.jetbrains.kotlinx/​kotlinx-coroutines-core@​1.8.1100100100100100

View full report

@socket-security

socket-security Bot commented Sep 10, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: maven junit:junit is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: android/gradle/libs.versions.tomlmaven/junit/junit@4.13.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/junit/junit@4.13.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@MrImmortal09

Copy link
Copy Markdown
Member Author

This PR removes the checks for the js/react-native as the backend code ie. fedimint-client-uniffi is no longer maintained. The checks would be added as soon as js/react-native gets backed by rust/fedimint-sdk.

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 8.55 MB

ℹ️ View Unchanged
Filename Size
js/shared/core/dist/index.js 1.97 kB
js/shared/core/dist/testing.js 97 B
js/shared/core/dist/testing2.js 3.03 kB
js/shared/types/dist/index.js 536 B
js/shared/types/dist/jsonClientConfig.js 75 B
js/shared/types/dist/utils.js 64 B
js/tools/create-fedimint-app/dist/index.mjs 17 kB
js/web/react/dist/index.js 1.31 kB
js/web/transport-web/dist/index.js 766 B
js/web/transport-web/dist/worker.js 890 B
js/web/wasm-bundler/fedimint_client_wasm_bg.wasm 4.26 MB
js/web/wasm-web/fedimint_client_wasm_bg.wasm 4.26 MB

compressed-size-action

@MrImmortal09
MrImmortal09 force-pushed the om/android-sdk branch 2 times, most recently from 4d24efe to 014b75a Compare September 10, 2026 16:59
@MrImmortal09 MrImmortal09 changed the title feat(android): Kotlin SDK from fedimint-sdk's uniffi feature feat(android): init Kotlin SDK from fedimint-sdk's uniffi feature Sep 10, 2026
@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review

@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review finding

Blocking: React Native packages can still be published, but the release workflow no longer builds or attaches their required artifacts.

In .github/workflows/changesets.yml, the publish job now only runs pnpm --dir js build before pnpm changeset publish (lines 44-49, 70). That root build excludes js/react-native/**, while @fedimint/react-native-bindings still exports lib/..., runs postinstall to download native binaries, and is still a publishable workspace package. There are also existing changesets for @fedimint/react-native-bindings / @fedimint/react-native, so the next Changesets run can publish them.

Before this PR, the workflow built RN Android/iOS artifacts, downloaded them into js/react-native/react-native-bindings, patched checksums into package.json, built build:reactnative, and uploaded release assets. With those steps removed, a published RN package can have missing lib output and no checksums, causing download-binaries.js to skip downloading native artifacts.

The same pattern exists in .github/workflows/snapshot.yml (lines 58-61): snapshots publish after only the web/wasm build.

Suggested fix: either fully remove/ignore/mark private the RN packages and clear their pending changesets, or keep the old RN build + checksum + artifact upload path until the packages are intentionally unpublished/retired.

Notes

I did not find an issue in the new Android Kotlin artifact layout during this pass. The PR CI is green; I did not rerun the native/Gradle jobs locally.

@MrImmortal09 MrImmortal09 changed the title feat(android): init Kotlin SDK from fedimint-sdk's uniffi feature feat(android): init Kotlin SDK from fedimint-sdk's uniffi feature and add tests for uniffi Sep 10, 2026
@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review

@MrImmortal09
MrImmortal09 requested a review from zeenix September 10, 2026 17:56
@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review result

No new blocking findings from this pass.

What I checked:

  • Re-reviewed the updated release path after chore: stop publishing react-native.
  • Confirmed @fedimint/react-native and @fedimint/react-native-bindings are now private and ignored by Changesets.
  • Ran pnpm --dir js changeset status --since origin/om/ffi-stage1; RN packages are no longer in the bump/publish set.
  • Re-inspected the new Android native/Kotlin workflows, generated binding scripts, Gradle module wiring, and Nix package names against the PR diff.

Notes:

  • My earlier publishing concern appears addressed by the latest commit.
  • I did not rerun the native cross-compile or Gradle build locally; I relied on the green Kotlin SDK CI jobs for those heavy checks.

@zeenix

zeenix commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

@MrImmortal09 IMO this is jumping the gun a bit. If you recall, we had decided to first get the SDK complete and working before this. If we build the high-level on top of it already, the rust SDK changes will now involve the binding work as well, slowing down the Rust SDK plans.

Nothing wrong with preparing already but I'd suggest making this work a WIP until we've the full SDK ready (i'm pretty confident we can get it done within the next few days).

@MrImmortal09

Copy link
Copy Markdown
Member Author

@zeenix I just thought that we should have something working on the go, so that we could test features against.
nvm I get your point marking this one as draft.

@MrImmortal09
MrImmortal09 marked this pull request as draft September 10, 2026 19:55
@zeenix

zeenix commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

@zeenix I just thought that we should have something working on the go, so that we could test features against.
nvm I get your point marking this one as draft.

Thanks, fwiw I also understand your motivation for doing this already.

@MrImmortal09
MrImmortal09 changed the base branch from om/ffi-stage1 to main September 17, 2026 13:33
@MrImmortal09 MrImmortal09 changed the title feat(android): init Kotlin SDK from fedimint-sdk's uniffi feature and add tests for uniffi feat(android): init Kotlin SDK (T15) Sep 17, 2026
@MrImmortal09 MrImmortal09 changed the title feat(android): init Kotlin SDK (T15) feat(android): init Kotlin SDK and expand uniffi coverage (T15) Sep 17, 2026
@MrImmortal09 MrImmortal09 changed the title feat(android): init Kotlin SDK and expand uniffi coverage (T15) feat(android): init android SDK and expand uniffi coverage (T15) Sep 17, 2026
@MrImmortal09
MrImmortal09 marked this pull request as ready for review September 17, 2026 14:48
@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review

Comment thread rust/fedimint-sdk/src/onchain.rs Outdated
Comment on lines +1703 to +1707
// The UniFFI views of `Operation<OnchainSendState>` and
// `Operation<OnchainReceiveState>`: `Operation<S>` is generic and UniFFI
// objects cannot be, so `crate::ffi::ffi_operation!` monomorphises
// one newtype object per state, forwarding every method to the real
// handle. See that macro's documentation in `ffi.rs`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick: please use the same line width limit (100 chars) for comments, as the rest of the code. Otherwise we unnecessarily have more LoC than needed. :)

Comment thread rust/fedimint-sdk/src/sdk.rs Outdated
Comment on lines +1714 to +1729
/// The UniFFI view of [`FederationInfo`], with `status` crossing as the
/// flattened projection above instead of the real [`FederationStatus`].
/// Exported as `FederationInfo`, for the same reason.
#[cfg(feature = "uniffi")]
#[derive(Debug, uniffi::Record)]
#[uniffi(name = "FederationInfo")]
pub struct FfiFederationInfo {
/// See [`FederationInfo::id`].
pub id: FederationId,
/// See [`FederationInfo::name`].
pub name: Option<String>,
/// See [`FederationInfo::network`].
pub network: Network,
/// See [`FederationInfo::status`].
pub status: FfiFederationStatus,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we please put all the FFI-specific/only items into a separate module hierarchy?

@MrImmortal09 MrImmortal09 Sep 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah sure , moving them rn.

@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review findings

I found two Android build blockers.

  • Kotlin DSL is no longer accepted by the configured plugin. The current CI failure is from android/app/build.gradle.kts:35:

    Using 'jvmTarget: String' is an error. Please migrate to the compilerOptions DSL.
    

    The same pattern exists in the library module at android/fedimint-sdk/build.gradle.kts:52-53, so fixing only the app module will likely just move the failure to :fedimint-sdk. Both modules should migrate from kotlinOptions { jvmTarget = "21" } to the Kotlin compilerOptions DSL, e.g. using JvmTarget.JVM_21.

  • CI installs JDK 17 while the Android modules request Java 21. .github/workflows/kotlin-sdk.yaml:94-98 sets up Temurin 17, but both Android modules set sourceCompatibility/targetCompatibility to JavaVersion.VERSION_21. There is no Gradle Java toolchain configured, so once the Kotlin DSL error is fixed the Gradle build is still set up to compile Java 21 sources with a Java 17 toolchain. Either set CI to Java 21 or add an explicit Gradle toolchain that provisions 21.

Checked:

Comment thread rust/fedimint-sdk/src/sdk.rs Outdated
Comment on lines +1638 to +1656
pub enum FfiFederationStatus {
/// See [`FederationStatus::Running`].
Running,
/// See [`FederationStatus::Recovering`].
Recovering,
/// See [`FederationStatus::Quarantined`].
Quarantined {
/// [`Diagnostic::code`](crate::Diagnostic::code).
code: crate::ErrorCode,
/// [`Diagnostic::message`](crate::Diagnostic::message).
message: String,
},
/// See [`FederationStatus::Closed`].
Closed,
/// See [`FederationStatus::Forgetting`].
Forgetting,
/// See [`FederationStatus::Forgotten`].
Forgotten,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we really need to duplicate the whole type? 🤔

@zeenix zeenix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review by Codex on behalf of @zeenix. Reviewed 554426a against T15 and the existing comments.

Requesting changes for the three inline correctness findings. The existing Android build findings
also remain: migrate both modules from kotlinOptions.jvmTarget to compilerOptions, and align
CI's JDK 17 with the modules' Java 21 target.

Two T15 requirements still need completing:

  • Move FFI-specific items into the requested module hierarchy. Keep common operation, subscriber
    and quote adaptation independent of UniFFI so T14 can reuse it. The current ffi_operation!
    hardwires UniFFI attributes, and the whole module is enabled only by the uniffi feature.
  • Add boundary conformance tests. This PR adds no behavioural tests; assembling the demo does not
    check repeated/concurrent quote submission, subscriber completion and cancellation, diagnostic
    preservation, or shutdown waking observers. Exercise those through generated bindings.

On the type/ownership questions, I audited all 23 new Rust types:

Types Count Assessment
Concrete operation wrappers 7 Justified: UniFFI objects cannot be generic
Subscriber wrappers 7 Justified: concrete types and serialized mutable access
Result records 4 Justified: generic operations and nested object references
Ecash detail projections 2 Justified: preserve opaque Notes and Rust field types
Federation status/info projections 2 Avoidable; see below
QuoteClaim 1 Reasonable shared single-use enforcement

For the duplication comment: export RawErrorDetails as a record, map DetailEnvelope to it
with uniffi::custom_type!, and export the existing Diagnostic, FederationStatus and
FederationInfo. This removes both federation projections while preserving structured details.
I compiled a minimal UniFFI 0.32 prototype and successfully generated Kotlin for this shape.
The complete T15 surface also needs the structured-details accessor on Error, which currently
exports only code() and reason().

The new Arc allocations for objects nested in records, Option and Vec are required by
UniFFI's conversion traits. As a small simplification, the three ffi_send parameters can take
&Quote: their bodies only borrow. This does not remove UniFFI's internal shared ownership.
I found no redundant new mutex.

Validation: cargo check --locked --features uniffi --tests passed, and I checked the failing
Android CI log. No end-to-end Kotlin tests were run.

Comment thread rust/fedimint-sdk/src/sdk.rs Outdated
Comment on lines +1664 to +1667
FederationStatus::Quarantined { diagnostic } => Self::Quarantined {
code: diagnostic.code,
message: diagnostic.message,
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review by Codex on behalf of @zeenix.

[P2] Preserve the diagnostic details when exporting quarantine status

This conversion copies only code and message, discarding diagnostic.details. Kotlin
callers of stored_federations(), federation_status() and status subscriptions consequently
lose structured data such as the conflicting module generations, and unknown detail envelopes
cannot survive the boundary as the contract promises.

Please carry the optional RawErrorDetails envelope using DetailEnvelope::to_raw().
The raw-envelope conversion also offers a way to remove these duplicated status/info types;
see the review summary.

withContext(Dispatchers.Main) {
lastInvoice = receive.invoice
lnReceiveCopy.isEnabled = true
watch(result, header) { receive.operation.updates().next() }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review by Codex on behalf of @zeenix.

[P2] Reuse one subscription for the lifetime of this watcher

The lambda calls updates() again on every iteration of watch. Each fresh subscription
immediately yields the current state, including a terminal state. The watcher therefore keeps
polling and allocating subscribers instead of waiting for transitions, and it never observes
the final null that ends the loop.

Create the subscriber once before starting watch, reuse its next(), and close it when the
watcher finishes. The recovery, deposit and withdrawal watchers have the same problem.

Comment on lines +306 to +308
val state = operation.awaitFinal()
"Redeemed ${formatMsats(notes.value())}\n\n" +
"operation ${operation.id()}\nstate: ${describeState(state)}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review by Codex on behalf of @zeenix.

[P2] Only report redemption after a successful terminal state

awaitFinal() also returns EcashReceiveState.Failed normally: operation failures are
states, while exceptions represent failures to observe the operation. This currently displays
"Redeemed" even when the notes were not redeemed.

Match the returned state and display the success message only for Done; display the failure
reason for Failed.

@zeenix

zeenix commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

On retiring the React Native lanes: please switch the tool instead of removing them. This PR repoints the Nix cross-compile from fedimint-client-uniffi to rust/fedimint-sdk's uniffi feature, and the lanes break only because uniffi-bindgen-react-native at 0.29.3 cannot read that library's uniffi 0.32 metadata. That pin is lifted in jhugman/uniffi-bindgen-react-native#468 (bump to 0.32). I built its branch head (Dzejkop:feat/uniffi-bump-to-0.32, 06f0a30) and ran it against libfedimint_sdk.so built with --features uniffi: it reads the metadata and generates the JSI bindings (TypeScript and C++), and the web bindings too. So the React Native packages can bind the same #[uniffi::export] surface this PR expands, from the same .so the new derivations produce.

Concretely, in this PR rather than a follow-up:

  • depend on the tool from that branch, pinned to a commit, in js/react-native/react-native-bindings/package.json instead of 0.29.3-1; its launcher builds the Rust CLI with cargo run on first use, and the package's prepare script calls yarn build, so check what pnpm does with that for a git dependency
  • point ubrn.config.yaml's rust.manifestPath at rust/fedimint-sdk/Cargo.toml with the uniffi feature, so the generated JSI bindings cover the new surface; the --no-cargo lanes already receive that library from Nix
  • keep .github/actions/build-rn-android, .github/actions/build-rn-ios, the react-native change filter and build-rn-* jobs in pull-request.yml and snapshot.yml, and the publish settings on the two js/react-native/* packages

T14 (the wasm binding) then builds on this PR: it adds the tool's web target and the small crate-side split the feature needs to compile on wasm32 (the tokio runtime pieces native-only, uniffi's wasm-unstable-single-threaded on wasm, browser storage in create_fedimint_sdk). None of that is needed here.

Generated by Claude Fable 5.1.

@MrImmortal09

MrImmortal09 commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

Regards wasm support , we actually don't know how the exact changes that are required by ubrn for the react-native support ( jhugman/uniffi-bindgen-react-native#468 yet to be merged ), so for the reason I have not included any wasm releated changes here.

@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review

@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review Finding

  • Blocking: duplicate UniFFI custom type registrations for Preimage and Timestamp.

    rust/fedimint-sdk/src/ffi/types.rs:60 and :65 add uniffi::custom_type! definitions for Preimage and Timestamp, but the same types are already registered under #[cfg(feature = "uniffi")] in rust/fedimint-sdk/src/types/preimage.rs:73 and rust/fedimint-sdk/src/types/timestamp.rs:44.

    With --features uniffi, Rust generates conflicting implementations of FfiConverter, Lower, Lift, TypeId, etc. for both types, so the crate cannot compile. This is what is failing both:

    • Run clippy, fmt, docs and wasm / Check clippy (uniffi feature)
    • Kotlin SDK / Native library / Cross-compile the .so via Nix

    Fix by keeping a single custom_type! registration per type, then rerun:

    cargo clippy --locked --lib --features uniffi -- -D warnings

I did not find a second concrete runtime issue worth blocking on in the sampled Android/FFI paths before this compile blocker is resolved.

@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review

@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review result

No new blocking finding from this pass.

What changed since my last review:

  • The duplicate UniFFI custom_type! registrations for Preimage and Timestamp appear resolved on current head 90492ee.
  • Rust SDK Continuous Integration is now passing, including Run clippy, fmt, docs and wasm.
  • The Android Gradle/JDK blocker I previously called out also appears addressed: both Android modules now use the Kotlin compilerOptions DSL with JvmTarget.JVM_17, and kotlin-sdk.yaml sets up JDK 17.

Current CI state I saw:

  • Passing: Rust SDK checks, JS lint/build/unit/typecheck/CLI, Socket checks.
  • Still running/pending at review time: Android native/Kotlin chain, web/doc jobs, SDK integration matrix.

I did not find a concrete additional runtime issue in the sampled FFI quote adapters, operation wrappers, generated-binding scripts, Android Gradle wiring, or demo lifecycle/threading paths. The broader maintainer review requesting T15 structure/test coverage changes still remains separate from this pass.

Verification note: I attempted a local cargo check --locked --features uniffi --tests, but cargo is not on this runner shell's PATH, so I relied on the current GitHub CI Rust results for that check.

@MrImmortal09

Copy link
Copy Markdown
Member Author

@zeenix PR is ready for review.

#353 is going to add more tests to this.

MrImmortal09 and others added 6 commits September 18, 2026 03:40
The React Native bindings were generated by uniffi-bindgen-react-native
against rust/fedimint-client-uniffi. That generator is pinned to uniffi
0.31 and cannot read the metadata rust/fedimint-sdk emits under uniffi
0.32 — it walks the record with the wrong layout and fails partway
through. So these lanes cannot be pointed at the new crate as they are,
and keeping them running against the old one would only be testing a
crate nothing is built on any more.

Removed rather than disabled: a workflow that is skipped indefinitely
still has to be read and reasoned about by everyone who touches CI, and
the git history is the better record of how this worked. The React
Native packages under js/ are untouched, so restoring these is a matter
of reinstating the workflows once the generator catches up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the android/ Gradle project (the fedimint-sdk library module that
becomes the AAR, plus a demo app), the cacheable Nix cross-compile that
produces its native libraries, and the scripts and CI that tie the two
together.

There is no hand-written Kotlin. The `#[uniffi::export]` blocks in
rust/fedimint-sdk hand out that crate's own types, so this SDK is a view
of that API rather than a copy that could drift from it. The surface is
the three Sdk methods that exist today — export_mnemonic, preview, join
— plus the constructor and the two value types they need.

Building it is two steps, and the split is the point:

  1. the native library, cross-compiled for both ABIs through
     nix/ffi.nix, which is expensive (rocksdb and aws-lc from C) and is
     not specific to any one language; and
  2. the bindings, which uniffi-bindgen reads out of the `.so` produced
     by step 1 rather than out of the crate source, so the Kotlin cannot
     describe a binary other than the one the device loads.

CI mirrors that exactly. android-native.yaml builds the `.so` and
uploads it; kotlin-sdk.yaml calls that workflow and generates against
the artifact. Keeping the costly half behind one callable workflow means
it is paid for once and a second binding generator added later starts
from the same binary. android-native.yaml has no pull_request trigger of
its own so that a PR touching both a Nix file and android/ cannot
cross-compile the same commit twice.

The Kotlin generation runs on plain cargo rather than Nix:
rust/uniffi-bindgen's only dependency is uniffi itself, which keeps that
whole job free of Nix, the NDK and any cross-compile toolchain.
`.#fedimint-uniffi-bindgen` builds the same binary but does not
currently work — crane's vendoring loses uniffi_bindgen's askama.toml.

nix-build.yml is replaced by android-native.yaml, which does the same
cache warming for the derivations this crate actually uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… shell

libclang's unwrapped clang was shadowing the host compiler for
host-target builds, and aws-lc-sys's bindgen feature was falling back
to a CMake build the crates.io tarball can't satisfy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…to UniFFI

Extends the opt-in `uniffi` feature (#384) from the three `Sdk` bootstrap
methods to the whole per-federation surface: `Federation`'s own methods,
its `ecash`/`lightning`/`onchain` facades, `AnyOperation` and the
`Operation<S>` handles each facade returns, and `Sdk::recover` /
`resume_recovery`. `ffi.rs` holds the two pieces of machinery shared
across facades: `ffi_operation!`, which monomorphises one UniFFI object
per concrete `Operation<S>` instantiation (the type is generic and a
UniFFI object cannot be), and `QuoteClaim`, the single-use guard every
quote type carries so a binding cannot submit the same quote twice —
`send` takes its quote by value in plain Rust, which already rules that
out at compile time, but a binding only ever holds a shared `Arc`.

Bearer values stay off the wire as bare strings: `Notes` (ecash) crosses
as a UniFFI object exactly like `InviteCode` and `Mnemonic` already do,
so a generated Kotlin/Swift record can never print the token through its
automatic `toString`. Records that hold notes by value in plain Rust
(`EcashSendDetails`, `EcashReceiveDetails`) get an `Arc<Notes>`-holding
projection exported under the real name instead, built through an
exhaustively-destructured `From` impl so a field added to the Rust
record fails to compile until its projection carries it too.

`Sdk::federation` and `Sdk::federation_status` now return `Result` over
FFI: both take a `FederationId` lifted from a plain string, and only a
fallible export turns a malformed one into this crate's own
`InvalidInput` rather than UniFFI's internal error. The renamed exports
this needed (`FederationStatus`, `FederationInfo` for the two
`Ffi*`-prefixed projections; snake_case for the handful that had drifted
to camelCase) keep the generated surface consistent with the rest.

nix/ffi.nix restores the macOS-only `libiconv` build input the earlier
rewrite of `buildOne` dropped, needed for `cc -liconv` to resolve in a
host build script's link step on macOS 14+.

scripts/build-android-sdk.sh now calls nix-build-android-so.sh and
generate-kotlin-bindings.sh — the same two scripts CI runs as
android-native.yaml and kotlin-sdk.yaml — instead of duplicating their
logic, so `just build-kotlin` can no longer drift from what CI does.
Only the cargo-ndk `--local` path is its own code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the step-numbered demo layout with one section per SDK call,
the way js/examples/vite-core lays its own demo out: wallet status
(open/restore/refresh, with a live balance from balanceUpdates()), join
(preview/join/recover), generate and pay a Lightning invoice, send and
redeem ecash, parse an invite code, generate a deposit address, send
on-chain, and recent activity. Every operation-returning call now
follows its own state to completion in its result line instead of
reading it once.

`onJoin` releases its old `Federation` handle only after a new one
exists, and treats `ALREADY_JOINED` as the cue to look the running
federation up by `sdk.federation(id)` rather than treat it as failure —
which is also what makes reattaching after a restart possible. Amount
fields convert sats to msats with an overflow check instead of wrapping.
Every section reads its `EditText`s on the main thread before its
`Dispatchers.IO` block runs.

Exercised against a live mutinynet federation on an emulator: open,
preview, join (including the already-joined path), invoice generation,
and deposit address generation all completed successfully.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MrImmortal09

Copy link
Copy Markdown
Member Author

@fedimint-bot review again

@fedimint-bot

Copy link
Copy Markdown
Collaborator

Review result

No new blocking finding from this pass.

Rechecked on current head bf8cc00:

  • The earlier duplicate UniFFI custom type issue is resolved here: Preimage and Timestamp now only have custom_type! registrations in rust/fedimint-sdk/src/ffi/types.rs; their type modules no longer register them separately.
  • The demo watcher issue appears addressed: recovery, lightning receive, deposit, and on-chain send now create one subscriber and pass it into watch(...), instead of calling updates() on each loop iteration.
  • The ecash receive demo now distinguishes EcashReceiveState.Done from Failed before saying notes were redeemed.
  • Android Gradle/JDK alignment is now Java 17 end to end (compileOptions, Kotlin compilerOptions, and kotlin-sdk.yaml).
  • Re-sampled the FFI operation wrappers, quote guards, error detail conversion, generated-binding scripts, and Android Gradle wiring.

Current CI state when checked:

  • Passing: Rust SDK tests, clippy/fmt/docs/wasm, fedimint pin check, Socket checks.
  • Still running: Android native/Kotlin chain and several JS/doc/integration jobs.

Caveat: I did not run the native cross-compile or Gradle build locally on this runner. The broader maintainer feedback about whether this PR should retire or preserve the React Native lanes remains a product/PR-scope decision separate from this pass.

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.

3 participants