Building an Android CoinSwap Taker: My Summer of Bitcoin Final Report
Running a multi-maker coinswap from a phone sounds simple until Tor, UniFFI, and Android lifecycle all show up at once. This is my Summer of Bitcoin 2026 final report for Citadel Tech / Citadel FOSS: how I built a Jetpack Compose CoinSwap taker that talks to the official Rust stack, discovers makers over Orbot, and completes real swaps on a physical device.
If you care about Bitcoin privacy tooling on mobile, or about wiring Rust protocol code into Android without rewriting it, this post walks through what I set out to do, what shipped, what is still open, and what I would do differently next time.
Pull request (submission): citadel-foss/coinswap-kotlin#2
Branch / fork: tanishkaa08/coinswap-kotlin (ffi-rpc)
What I set out to do
CoinSwap already had a working desktop taker flow. The gap was mobile: there was no first-class Android client that could:
- restore / unlock a taker wallet on-device
- sync balances and UTXOs
- discover makers over Tor
- prepare and execute Taproot or Legacy coinswaps
- recover from interrupted swaps
- match the mental model of the desktop taker without reimplementing protocol logic in Kotlin
My goal was not a demo UI on top of stubs. I wanted a phone app that uses the same Rust taker via generated UniFFI bindings (libcoinswap_ffi.so), so protocol correctness stays in core-lib while Android owns UX, lifecycle, Tor plumbing, and local testing tooling.
What I built
I shipped a full Compose taker app with this stack:
1
2
3
4
5
6
7
Compose UI
→ ViewModels
→ CoinswapRepository
→ Generated UniFFI Kotlin (org.coinswap)
→ libcoinswap_ffi.so
→ Rust Taker
→ Electrum (electrs) + Tor (Orbot SOCKS)
App surfaces
- Login / setup — wallet passcode and session restore; Electrum backend for Citadel signet or local regtest
- Home — balances, sync, Tor status, UTXO list with privacy cues
- Receive / send — ordinary wallet spends and QR receive
- Markets — offerbook sync and live maker list over Tor
- Swap — Taproot or Legacy, amount, maker count, onion filtering, coin control (auto by default, manual opt-in)
- Swap progress — foreground service + notification while a swap runs; phase text from
swap_tracker.cbor - Recovery — path for interrupted / failed taker swaps
- History / reports — wallet transactions and local coinswap reports
- Settings — Tor / Orbot helpers, reconnect, wallet data reset
Backend glue
CoinswapRepositorywraps UniFFITaker(init, sync, offers, prepare/start/recover, reports, sends)TakerHolderowns the live native instance with lease-aware close and FFI mutexes- Activity-scoped swap ViewModel +
SwapForegroundServiceso an in-flight swap survives tab switches and backgrounding - Config / Tor helpers aligned with the desktop taker model (Orbot on
127.0.0.1:9050)
Regtest / phone tooling
Under scripts/ I added helpers for local phone testing: WSL maker bring-up, Tor SOCKS/control portproxy on Windows, adb reverse for connectivity, and quick probes. I exercised multi-maker coinswaps on a physical phone against Docker makers on regtest.
Major technical / design decisions
1. UniFFI over rewriting the protocol
The important boundary: wallet and swap logic stay in Rust. Kotlin is an adapter. That kept the Android app honest to upstream coinswap behavior and avoided a second, divergent protocol implementation.
2. Electrum backend instead of Bitcoin Core RPC on the phone
Early drafts mirrored a Core RPC + ZMQ desktop setup. For a real mobile path we moved to Electrum (electrs) via BackendConfig. Clearnet Electrum connects directly; maker discovery and swaps still go through Orbot. That matches how a phone should talk to the chain without running bitcoind on-device.
3. Orbot-only Tor — remove in-app Tor
I initially explored embedded Tor. In practice it dropped proof-of-funding mid-swap (UnexpectedEof / SOCKS timeouts) and fought Orbot for port 9050. The durable choice: no in-app Tor daemon. Probe Orbot’s SOCKS on 127.0.0.1:9050, optionally auth on the control port, and fail clearly when Orbot is not ready.
4. Lease-safe native lifecycle
Taker is a scarce native resource. Concurrent ViewModels (wallet sync, markets, swap) can race init/close. TakerHolder uses:
withLeasedso logout / reconnect cannotclose()a Taker still in useinitMutex/ffiMutex/longOpMutexso init, short reads, and long Tor/swap ops do not interleave unsafely
5. Foreground service for swap execution
Coinswaps are long-running. Keeping execution only in a Compose-scoped coroutine was fragile. A foreground service runs startCoinswap, publishes progress on SwapExecutionBus, and polls the core-lib tracker file so the UI and notification stay in sync when the user leaves the Swap tab.
6. Honest Tor / estimate UI
Configurable SOCKS fields that the native stack ignored were removed or fixed. Offerbook sync failures gate swap prep. Maker estimates reject empty / ineligible selections instead of inventing fake fees. On a money app, that honesty matters more than polish.
What shipped
Delivered in PR #2:
| Area | Status |
|---|---|
| Compose taker UI (login → home → markets → swap → history → settings) | Shipped |
UniFFI Taker integration (Electrum + Orbot) |
Shipped |
| Multi-maker coinswaps on physical phone + regtest makers | Tested end-to-end |
| Wallet sync, receive, send, recovery paths | Exercised in the same setup |
| Swap foreground service + progress tracking | Shipped |
| Local scripts for makers / Tor / adb reverse | Shipped |
| Security and correctness hardening from review (leases, path validation, cancellation, session clearing, backup excludes) | Iterated across review rounds |
The PR title and description summarize the first cut; later commits simplified the UI, removed in-app Tor, and hardened FFI concurrency and swap gates.
What remains
Still open or intentionally deferred:
- Merge + polish — the PR is still open; remaining review nits (fee display edge cases, packaging) can land after maintainer feedback
- Keystore-backed secrets — move remaining sensitive prefs fully onto Android Keystore
- Product packaging — branded package name, release signing, clearer first-run Orbot onboarding
- Mainnet readiness — more soak testing outside regtest/signet, fee-rate wiring where the native API supports it, tighter recovery heuristics
- CI — automated instrumented / contract tests beyond the unit contracts added during review
What I learned
- FFI is a product surface. Binding generation, ABIs, JNA load paths, and native close semantics are as much of the app as Compose screens.
- Mobile Bitcoin is mostly systems work. Tor readiness, foreground services, coroutine cancellation, and device networking matter as much as the swap form.
- Upstream alignment beats clever rewrites. Mirroring the desktop taker’s Electrum + Orbot model removed entire classes of failure.
- Review is part of the build. Iterating on mentor and automated review feedback forced leases, honest Tor probes, and safer session handling I would have deferred.
- Privacy UX is concrete. Coin control defaults, UTXO privacy cues, and not claiming Tor when Orbot is not up are user-trust features, not cosmetics.
What happens next
- Address remaining review comments and work toward merging PR #2 into
citadel-foss/coinswap-kotlin. - Keep the Android taker in sync as coinswap-ffi (Electrum branch) evolves.
- Continue contributing to Citadel’s coinswap mobile / FFI stack after SoB — especially release hardening, recovery UX, and test coverage.
- Document a shorter “phone + Orbot + electrs” contributor path so others can reproduce the regtest setup without reverse-engineering my scripts.
Conclusion
Summer of Bitcoin let me turn a desktop-only CoinSwap taker story into something you can hold: an Android client that keeps protocol logic in Rust, privacy routing in Orbot, and UX in Compose. The hardest parts were not the screens — they were lifecycle, Tor honesty, and making UniFFI safe under concurrent ViewModels.
If you want to try the work, follow PR #2, skim the README on the ffi-rpc branch, and ping me with review notes or repro steps. Contributions, questions, and tough bug reports are all welcome.
Share / tags: #SummerOfBitcoin #Bitcoin #OpenSource
Thanks to my mentors and the Citadel / CoinSwap community for the reviews, regtest war stories, and patience while Tor and UniFFI fought me on a real phone.