Zapp is a privacy-first Zcash wallet with an end-to-end encrypted peer-to-peer messenger built
into it. Shielded ZEC for the money, Hyperswarm for the messages, and a peer-to-peer offramp for
spending in local currency. No account, no custody, no tracking. Crash reporting on Play builds
is opt-in and off by default, and the foss flavor has no Google dependency at all.
Zapp is a privacy-focused Android wallet forked from Zodl,
itself a fork of Electric Coin Company's
Zashi. The fork diverged at upstream
commit 2409c4d7
(the zodl 3.3.1 release merge) and carries roughly 500 commits of divergence on top of it. What the
fork has taken from upstream since, and what it still owes, is tracked in
docs/audits/UPSTREAM-PARITY.md.
- Encrypted P2P chat. Direct and group messaging over Hyperswarm, with Ed25519 identities derived from the wallet seed. No Zapp server can read your messages: a project-run blind peer stores encrypted blocks so they reach a recipient who was offline, and it cannot decrypt them.
- A peer-to-peer offramp. Scan a merchant QR and pay in local currency out of shielded ZEC, through the p2p.me contracts on Base. Seven currencies are wired up: INR, BRL, IDR, ARS, VEN, NGN, COP.
- A different shell and visual system. Three tabs (Pay | Chats | You), a Swiss-minimalist token
set (
ZappTheme), flat geometry, and bottom-left back navigation. - No governance voting. Upstream's voting feature is intentionally not carried.
Everything else is deliberately kept in lockstep with upstream: Kotlin package names, module names,
Gradle flavors, resource ids and build-property names all stay as zodl-android has them so that
future upstream merges stay cheap.
- Shielded-by-default Zcash send and receive, with unified, transparent and shielded addresses
- Ironwood (NU6.3) compatible, including a per-pool balance breakdown on the home screen
- Balance history chart, transaction history, filters, notes and tax export
- Address book, with the same records backing chat contacts
- Encrypted chat: DMs, groups, media, read receipts, payment requests
- Keystone hardware-wallet signing over animated QR
- Swaps to and from shielded ZEC through NEAR intents
- Flexa payment integration, present and kept merge-compatible with upstream but inert:
ZCASH_FLEXA_KEYships blank so the SDK is never initialised - Top Up shows your own receive addresses (from an exchange, or from another wallet) instead of handing you to a third-party on-ramp
- Tor routing on by default for exchange-rate lookups, transaction submission and integrations, with a settings toggle to turn it off
- Background chat notifications: contentless FCM topics on the Google-enabled
storeandinternalflavors, a self-hosted doorbell with no Google dependency onfoss(see docs/notifications/) - English, Spanish, Portuguese and Indonesian
TODO: add current screenshots. The inherited upstream store screenshots were removed from this repository and have not been replaced yet.
settings.gradle.kts includes two sibling projects by path, so both must be checked out next
to this repository on disk:
include(":zappmessaging")
project(":zappmessaging").projectDir = externalProjectDir("../zappMessaging/android", "zappMessaging/android")
include(":bare-kit")
project(":bare-kit").projectDir = externalProjectDir("../bare-kit/android", "bare-kit/android")bare-kit is public: it is an unmodified clone of holepunchto/bare-kit
at the commit pinned in .zapp-deps (a v2.0.0 checkout).
JustZappIt/zappMessaging is private at the time of writing. Gradle fails at configuration
time if it is missing, and there is no flag that stubs the messaging module out, so people outside
the JustZappIt org cannot currently configure or build this project. CI works only because it
checks the repository out with a read token (ZAPP_MESSAGING_READ_TOKEN). This is a known
limitation, stated here rather than worked around.
For the same reason, this repository ships without the GitHub Actions workflows: every job depends on that private checkout and on repository secrets, so none of them could run here. The references to CI elsewhere in this document describe the internal repository, not something you can run from this tree.
- JDK 17. This is the only supported JDK:
JVM_TOOLCHAIN=17ingradle.properties,gradle/daemon-jvm.propertiespins the Gradle daemon to Temurin 17, and CI reads the same property. In Android Studio, set Settings > Build > Build Tools > Gradle > Gradle JDK to a JDK 17 install rather than the bundled JBR. - Android SDK platforms 36 (this repo) and 34 (required by
bare-kit), NDK27.0.12077973(this repo) and NDK28.2.13676358plus CMake 4.1.2 (required bybare-kit's native build). - Node.js, for
bare-kit's npm dependencies. adbonPATH, and an emulator or device.
Exact versions of everything else are in Build environment | exact versions below.
<some-dir>/
zodl-android/ <-- this repository
zappMessaging/ <-- required, currently private
bare-kit/ <-- required, public
From the repository root, with both siblings pinned to the SHAs recorded in
.zapp-deps:
git clone https://github.com/JustZappIt/zappMessaging.git ../zappMessaging
git -C ../zappMessaging checkout "$(grep '^zappMessaging=' .zapp-deps | cut -d= -f2)"
git clone https://github.com/holepunchto/bare-kit.git ../bare-kit
git -C ../bare-kit checkout "$(grep '^bare-kit=' .zapp-deps | cut -d= -f2)"
(cd ../bare-kit && npm ci)
./gradlew :app:installZcashmainnetFossDebug
adb shell monkey -p xyz.justzappit.zapp.foss.debug -c android.intent.category.LAUNCHER 1The foss flavor is the easiest local build: it needs no google-services.json and pulls in no
Firebase or ML Kit. CI checks the two siblings out at the same SHAs and fails the build if they do
not match .zapp-deps.
If you change the messaging JavaScript in ../zappMessaging, rebuild its worklet bundle with
npm run build:android there before running Gradle again.
Variants are the product of two flavor dimensions and the build type:
| Dimension | Values | Effect |
|---|---|---|
network |
zcashmainnet, zcashtestnet |
Testnet adds a .testnet application-id suffix, so both install side by side |
distribution |
store, foss, internal |
foss adds .foss and drops Google dependencies, internal adds .internal |
So a testnet FOSS debug build installs as xyz.justzappit.zapp.testnet.foss.debug, and a mainnet
one as xyz.justzappit.zapp.foss.debug.
ZCASH_NETWORK in gradle.properties is committed blank, which registers both networks so the
two apps can be built and installed side by side. That is also what CI expects. Set it to
mainnet or testnet if you want the short task names to resolve to one network, and put that
override in local.properties rather than committing it:
./gradlew :app:assembleZcashtestnetFossDebug # spell the variant out
./gradlew -PZCASH_NETWORK=testnet :app:assembleDebug # or narrow the matrixThe selection drives both BuildConfig.FLAVOR_network and the runtime R.bool.zcash_is_testnet
resource, so the two cannot drift. Testnet builds default to
lightwalletd.testnet.cipherscan.app:443 (see
ui-lib/.../common/provider/LightWalletEndpointProvider.kt).
Release builds are signed with a key held by the maintainers. The build reads
ZCASH_RELEASE_KEYSTORE_PATH, ZCASH_RELEASE_KEYSTORE_PASSWORD, ZCASH_RELEASE_KEY_ALIAS and
ZCASH_RELEASE_KEY_ALIAS_PASSWORD from a git-ignored local.properties; they are committed blank
in gradle.properties, and blank means an unsigned release build. Debug builds need none of this.
| Module | What it is |
|---|---|
app |
Application shell, flavors, signing, manifest merge |
ui-lib |
Screens, view models and navigation, including the chat, swap and offramp flows |
ui-design-lib |
ZappTheme tokens and the shared Compose component set |
sdk-ext-lib |
Extensions over the Zcash Android SDK |
evm-lib |
Kotlin Multiplatform EVM primitives: ABI coding, secp256k1 signing, HD derivation, JSON-RPC |
offramp-lib |
Kotlin Multiplatform p2p.me client: order lifecycle, fees, QR parsing, revert decoding |
preference-*, configuration-*, crash-*, spackle-*, build-info-lib |
Upstream plumbing, kept as upstream has it |
test-lib, ui-integration-test, ui-screenshot-test, ui-benchmark-test |
Test and benchmark harnesses |
build-conventions-secant |
Gradle convention plugins shared by every module |
:zappmessaging, :bare-kit |
The two external siblings described above |
Deeper notes on the design of the build and the app live in docs/Architecture.md.
Contributions are welcome. Please read the Contributing Guidelines first: they cover the Developer Certificate of Origin sign-off and the commit-message format.
- The default branch is
main, and all pull requests target it. - Branch names follow
feat/<name>,fix/<name>,chore/<name>,docs/<name>. - Commit titles follow Conventional Commits, for example
fix(chat): stop dropping the last read receipt. - Run
./gradlew ktlint detektAllbefore opening a pull request, which is what CI runs.
Build setup end to end, including troubleshooting, is in DEVELOPER_SETUP.md and docs/Setup.md.
File a GitHub issue in this repository for Zapp-specific bugs. For upstream Zcash or Zodl issues, use Zodl issues or the Zcash Forum.
Do not open a public issue for a vulnerability. The reporting process, scope and disclosure expectations are in SECURITY.md.
Everyone participating in this project is expected to follow the Contributor Code of Conduct.
Most build failures on a fresh machine come down to a mismatched JDK, a missing NDK, or the sibling
repositories not being checked out next to zodl-android. These are the versions this tree is
known to build with.
| Tool | Version | Notes |
|---|---|---|
| OS | macOS on Apple Silicon | Linux and Windows are supported upstream but are not verified on this fork. |
| Android Studio | Narwhal (AI-253) or newer |
Anything new enough for AGP 8.13 should work. Set the Gradle JDK to 17. |
| Tool | Version | How it is selected |
|---|---|---|
| JDK | 17 | JVM_TOOLCHAIN=17 in gradle.properties, gradle/daemon-jvm.properties pins the daemon to Temurin 17, and the CI setup-java action reads JVM_TOOLCHAIN. Point JAVA_HOME at a JDK 17 install for the first ./gradlew run on a machine where Gradle has not auto-provisioned one. |
| Android JVM target | 1.8 (ANDROID_JVM_TARGET), Kotlin JVM target 8 (KOTLIN_JVM_TARGET) |
Android does not support bytecode targets beyond Java 8. Do not change these. |
Failed to read key AndroidDebugKey from store ...debug.keystore or
Algorithm HmacPBESHA256 not available means JAVA_HOME is pointing at a JDK older than 17.
Upgrade it, or delete ~/.android/debug.keystore and let it regenerate.
| Tool | Version | Notes |
|---|---|---|
| Gradle | 8.14.4 | Pinned by the wrapper. Always use ./gradlew, never a system gradle. |
| Android Gradle Plugin | 8.13.2 (ANDROID_GRADLE_PLUGIN_VERSION) |
|
| Kotlin | 2.3.10 (KOTLIN_VERSION) |
|
| Compose compiler | 1.5.15 (ANDROIDX_COMPOSE_COMPILER_VERSION) |
|
| Detekt | 1.23.8 | |
| ktlint | 1.8.0 |
| Component | Version |
|---|---|
compileSdk |
36 (ANDROID_COMPILE_SDK_VERSION) |
targetSdk |
35 (ANDROID_TARGET_SDK_VERSION) |
minSdk |
27 (ANDROID_MIN_SDK_VERSION) |
| NDK, this repository | 27.0.12077973 (ANDROID_NDK_VERSION) |
NDK, bare-kit module |
28.2.13676358 (declared in ../bare-kit/android/build.gradle) |
| CMake | 4.1.2 (bare-kit requires 4.0.0+) |
Both NDK versions need to be installed: this repository's modules pin the first, the sibling
bare-kit module pins the second for its own native build.
Pinned in gradle.properties, or directly in the version catalog in settings.gradle.kts, and
resolved by Gradle. You do not install these, but when a resolution error names one of them, these
are the versions in play.
| Area | Library | Version |
|---|---|---|
| Compose | androidx.compose.ui / foundation, material3, material-icons |
UI/Foundation 1.10.4, Material3 1.4.0, Icons 1.7.8 |
| AndroidX | activity, lifecycle, navigation-compose, fragment |
1.12.4, 2.10.0, 2.9.7, 1.8.9 |
| AndroidX | core-ktx, splashscreen, work-runtime, browser |
1.17.0, 1.2.0, 2.11.1, 1.9.0 |
| AndroidX | camera-camera2 / lifecycle / view |
1.5.3 |
| AndroidX | biometric |
1.4.0-alpha05 |
| AndroidX | biometric-ktx |
1.4.0-alpha02 (pinned literally in settings.gradle.kts) |
| AndroidX | security-crypto |
1.1.0 |
| DI | io.insert-koin:koin-android |
4.1.1 |
| Coroutines | org.jetbrains.kotlinx:kotlinx-coroutines-* |
1.10.2 |
| Serialization | kotlinx-serialization-json |
1.10.0 |
| Images | io.coil-kt:coil-compose |
2.6.0 |
| Location | com.google.android.gms:play-services-location |
21.3.0 |
QR scan (store) |
com.google.mlkit:barcode-scanning |
17.3.0 |
| QR render | com.google.zxing:core |
3.5.4 |
| Zcash | cash.z.ecc.android:zcash-android-sdk |
2.8.0-rc.1-SNAPSHOT (Ironwood / NU6.3) |
| Zcash | cash.z.ecc.android:kotlin-bip39 |
1.0.9 |
| Crypto | com.google.crypto.tink:tink-android |
1.20.0 |
| Keystone | com.github.KeystoneHQ:keystone-sdk-android |
0.8.3 |
| Flexa | co.flexa:core / spend |
1.1.3 |
| Animations | com.airbnb.android:lottie-compose |
6.6.4 |
| Desugaring | com.android.tools:desugar_jdk_libs |
2.1.5 |
To confirm any of these locally:
./gradlew :ui-lib:dependencies --configuration zcashmainnetFossDebugRuntimeClasspath.
The Zcash SDK resolves from Maven snapshots, matching upstream: SDK_INCLUDED_BUILD_PATH is blank
in gradle.properties and ZCASH_SDK_VERSION is requested from the Sonatype snapshot repository.
No sibling SDK clone and no local Rust build are needed. To build against a local SDK checkout, set
SDK_INCLUDED_BUILD_PATH=../zcash-android-wallet-sdk in local.properties, never in
gradle.properties, because the committed default has to match upstream.
-
WhileSubscribed(Duration)does not resolve. UsingSharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT)with akotlin.time.Durationneedsimport kotlinx.coroutines.flow.WhileSubscribed. Without it the compiler picks theLongoverload and fails with "actual type is Duration, but Long was expected". -
MaterialColorsis not available inui-lib.com.google.android.materialis not a declared dependency there. Use a color resource, or parse the hex directly. -
Android Studio's bundled JDK 21 poisons a CLI JDK 17 build. If the IDE compiles the build logic with JBR 21, a later command-line build on JDK 17 fails with
class file version 65.0 ... only recognizes ... up to 61.0. Set the IDE's Gradle JDK to 17, and cleanbuild-conventions-secant/{build,.gradle,.kotlin}before rebuilding. -
A shared Gradle build cache poisons the precompiled-script-plugin compile.
~/.gradle/caches/build-cache-1is per machine, not per project. This fork'ssettings.gradle.ktsincludes two projects that upstreamzodl-androiddoes not, so:build-conventions-secantproduces a different plugin-spec hash. Building both repositories on one machine without isolation makes kotlinc fail withSource file or directory not found: .../_<hash>/PluginSpecBuilders.kt. Recover with:./gradlew --stop rm -rf ~/.gradle/caches/build-cache-1 \ build-conventions-secant/{build,.gradle,.kotlin} \ .gradle ./gradlew :app:installZcashtestnetFossDebug --no-build-cacheLong term, either pass
--no-build-cachewhen switching repositories, or give each repository its ownGRADLE_USER_HOME.
Upstream plumbing parity is deliberate, and drift is expensive. Before porting anything, read docs/audits/UPSTREAM-PARITY.md, which is the single living record of what has been taken from upstream, what is still owed, and what was investigated and dismissed. Process for a sync lives in docs/FORK_MAINTENANCE_GUIDE.md, and security-relevant upstream changes have their own procedure in docs/UPSTREAM_SECURITY_SYNC_GUIDE.md.
- Builds print "Unable to detect AGP versions for included builds. All projects in the build
should use the same AGP version." This is safe to ignore. The version used under
build-conventions-secantis the same one used everywhere else. - With
IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLEDset, the debug APK cannot be run. Only set it for automated test runs. - Compose test coverage reads low, because of known limitations in how Jacoco sees Compose.
- Adding
espresso-contribbreaks the build through conflicting classes. This is a known issue in the Zcash Android SDK. - On first launch,
AndroidKeysetManager: keyset not found, will generate a new oneis printed twice. It is not an error and the code is not running twice.
If you are forking to build a different app rather than to contribute back:
- Change
ZCASH_RELEASE_APP_NAMEin gradle.properties. - Change
ZCASH_RELEASE_PACKAGE_NAMEin gradle.properties, which app/build.gradle.kts reads to build every application id. - Change
support_email_addressin strings.xml. - Remove the copyrighted Zcash and Zapp icons and logos, which live under
ui-lib/src/main/res/ui/common/andui-lib/src/main/ic_launcher-playstore.png. - Optionally, configure Continuous Integration secrets, and Firebase keys at
app/src/debug/google-services.jsonandapp/src/release/google-services.jsonif you want thestoreflavor.
Zapp is a fork of the MIT-licensed Zashi / Zodl Zcash wallet, so the project as a whole is available under the MIT License (LICENSE-MIT); all code inherited or derived from upstream (© 2021-2025 Zcash) stays MIT and keeps the upstream copyright notice.
Zapp-original contributions, including the evm-lib and offramp-lib
modules and the P2P chat feature, are dual-licensed under MIT OR Apache-2.0
(LICENSE-APACHE), matching the wider Zcash/Rust ecosystem
convention and adding an explicit patent grant on Zapp's own code. Such files
carry an SPDX-License-Identifier: MIT OR Apache-2.0 header; treat any
unmarked file as MIT-only. See LICENSE and NOTICE for the
full breakdown, and docs/Third party licenses.md
for bundled fonts and third-party dependencies.