A native, self-custodial mobile wallet for eCash.
A native mobile wallet for eCash — the Layer Two Labs Bitcoin
hardfork that activates Drivechain (BIP300/301) and airdrops eCash 1:1 to BTC holders. One Swift +
SwiftUI codebase ships as a native SwiftUI app on iOS and native Jetpack Compose on Android via
Skip. All key material, signing, and consensus logic is handled by
BDK (bdk-swift / bdk-android).
Multi-wallet and multi-network from day one. Bundled networks: Bitcoin mainnet (0'),
L2L Signet (1'), and the eCash dry-run chain (drynet2, unit ECX); further eCash / L2L
networks slot in as NetworkRegistry entries.
We're not accepting outside code contributions right now. The codebase is under heavy active development and changing quickly, and we'd like it in a more stable place before opening it up to pull requests.
Feature ideas are very welcome, though — open an issue describing what you'd like to see and we'll take it under consideration. Bug reports are appreciated too.
| Layer | Choice |
|---|---|
| Language / UI | Swift 6 · SwiftUI → Compose (Skip) |
| Wallet engine | BDK 2.3.x (bdk-swift on iOS, bdk-android on Android) |
| Secure storage | SkipKeychain (iOS Keychain / Android Keystore) — mnemonics only |
| Local storage | BDK-owned SQLite (chain data) + JSON wallet-list metadata (no SkipSQL) |
| Min OS | iOS 26+ · Android 9+ (API 28) |
ECashWalletMobile/ ← the Skip Fuse app (native Swift): UI, view models, state
Sources/ECashWalletMobile/ App/ · DesignSystem/ · Screens/ · Components/ · Resources/
Packages/WalletService/ ← the BDK seam, a SEPARATE transpiled (Skip Lite) package
Sources/WalletService/ WalletEngine, BDKWalletEngineFactory, NetworkRegistry,
WalletManager, KeyStore, WalletStore, Descriptors, Models…
Darwin/ ← iOS app target
Android/ ← generated Compose output + Kotlin glue
The app is Fuse (native Swift compiled for Android); WalletService is Lite (transpiled to
Kotlin) so it can call bdk-android directly. This split is the one architectural subtlety — see
CLAUDE.md §5.
Known-good toolchain (2026-09-23):
| Tool | Version |
|---|---|
| macOS | 26.7 |
| Xcode | 27.0 (Swift 6.4) — iOS 27 simulator runtime |
| Skip | 1.9.11 (CLI and the SwiftPM skip package — keep them in lockstep) |
| Swift Android SDK | 6.4.0 (NDK r30) |
| JDK | Homebrew openjdk@25 (Gradle) |
- macOS with Xcode. In Xcode 27 the simulator app is DeviceHub
(
Xcode.app/Contents/Applications/DeviceHub.app) — there is noSimulator.app. - Android Studio + Android SDK, with an emulator created (Device Manager) and running before you launch the Android app.
- Skip CLI:
brew install skiptools/skip/skip, then verify the toolchain withskip checkup. - Swift Android SDK:
skip android sdk install --version 6.4.0(the full version —6.4finds nothing). It must match Xcode's Swift version: the Android cross-compile still reads Xcode's macOS SDK, so an older Android toolchain breaks as soon as Xcode moves a major version. Keep only one Android SDK installed (swift sdk list/swift sdk remove) — with several, SwiftPM refuses to pick one ("matched multiple SDKs"). - JDK for the Gradle / Robolectric tests:
JAVA_HOMEmust point at a real JDK, e.g.export JAVA_HOME=/opt/homebrew/opt/openjdk@25. Android Studio's bundled JDK isn't used.
Warning
Building from source isn't recommended yet. The wallet is under heavy active development — APIs, storage layout, and screens change frequently, and it targets test networks by default. If you build anyway, treat it as a preview: don't put real funds in a wallet built from an in-development checkout. (Bitcoin mainnet is selectable but the app is pre-release.)
# Both platforms at once (iOS Simulator + running Android emulator):
skip app launch
# iOS: open the workspace in Xcode and run the "ECashWalletMobile App" target.
open Project.xcworkspace
# Android only (quick): build the debug APK and install to the running emulator:
skip export --debug
adb install -r .build/skip-export/ECashWalletMobile-debug.apkiOS logs appear in the Xcode console; Android logs in Android Studio's Logcat or adb logcat.
Simulator and Android builds need no signing setup. To run on a real iPhone, supply your own Apple Developer Team ID — it's deliberately not committed:
cp Darwin/DeveloperSettings.xcconfig.example Darwin/DeveloperSettings.xcconfig
# edit the file and set DEVELOPMENT_TEAM = <your team id> (Xcode → Settings → Accounts)That file is gitignored; the project's xcconfig includes it optionally, so signing then works in
Xcode, xcodebuild, and fastlane. With a device connected + trusted, you can build/install/launch
from the command line:
scripts/run-ios-device.sh # Debug (default)
scripts/run-ios-device.sh Release # Release# Fast: host tests for the app view models + Thunder crypto (Swift Testing, on the Mac).
swift test
# The wallet engine package — transpiled JUnit on the JVM (Robolectric), so both platforms in one run:
swift test --package-path Packages/WalletService
# Verify the Android build actually transpiles + compiles (the real Android gate):
skip export --debug
# Real-BDK integration on a device runtime:
ANDROID_SERIAL=emulator-5554 swift test # Android instrumented
WALLETSERVICE_LIVE=1 swift test --filter testLiveSignetSync # opt-in live L2L Signetswift build only checks Apple + transpilation; skip export --debug is the real Android check.
For a release build, scripts/build-apk.sh also verifies the Swift runtime is bundled (42 native
libs incl. libswiftCore.so).
Note
Known issue on Xcode 27: the root package's swift build / swift test currently fails with
"linked as a static library … duplication of library code" (SwiftPM 6.4 is stricter about the Skip
packages' static products). It is not caused by Skip, and it doesn't affect the Xcode iOS build or
skip export. swift test --package-path Packages/WalletService is unaffected.
No change to WalletService or a view model merges without tests in the same PR.
Your keys stay on your device, and the app is built to keep them exposed as little as possible.
- Keys never leave the device. The recovery phrase (and only the phrase) is stored in the OS
secure store — iOS Keychain (
WhenUnlockedThisDeviceOnly, no iCloud sync) / Android Keystore-backed encrypted storage — keyed per wallet. Everything else (xpub descriptors, labels, the transaction cache) is public data. - BDK owns all cryptography for the Bitcoin/eCash wallet. Key derivation, signing, PSBT building, and coin selection are handled by the Bitcoin Dev Kit — no hand-rolled crypto. (The one exception is the in-development Thunder sidechain engine — a non-BDK chain, not yet user-facing — which uses swift-crypto + BLAKE3 with derivation and serialization pinned to published test vectors. See the acknowledgements table.)
- Watch-only + sign-on-demand. Day-to-day the wallet runs watch-only: balance, address derivation, syncing, and even building a transaction use only the public descriptors — the secret store is never read. Your phrase is loaded for exactly one purpose, at one moment: to sign a transaction you've confirmed. It's pulled into a transient in-memory signer, used, and dropped — so the private key's lifetime in memory is a single signing operation.
- Nothing secret on disk, nothing secret in logs. Only public data is persisted. Errors are typed and scrubbed before they reach the UI or any log — a signing failure says "signing failed," never the key (enforced by an automated no-leak test).
- Device-auth gates. Optional biometric/passcode lock on launch and on returning to the foreground (with a configurable grace window so a quick trip to another app doesn't re-prompt), plus an independent auth gate on the confirm-send step. On a device with no biometric/passcode enrolled the gate passes through rather than locking you out.
- Guarded backup. Revealing the phrase is behind an explicit gate + device auth, and you confirm a few words before it's marked backed up. Screenshots aren't blocked — capturing your own recovery phrase is your call (the app advises against it) — though the app-switcher snapshot stays obscured.
- Spendable balance (0-conf policy). Only confirmed coins + your own unconfirmed change are
treated as spendable. Coins you received that are still unconfirmed (0-conf) are shown separately
as pending and are kept out of coin selection until they confirm — because an unconfirmed
incoming payment can still be double-spent or RBF-replaced, which would orphan anything you tried to
send on top of it. This mirrors Bitcoin Core's trusted/untrusted rule (BDK gives us the
confirmed/trustedPending/untrustedPendingsplit; the send path also marks untrusted outpoints unspendable). A configurable confirmation threshold — and potentially a per-network one (e.g. stricter on mainnet than on testnets) — is a planned option. - Clean removal. Removing a wallet purges its phrase from the secure store plus all of its on-device data.
The decision records behind this live in docs/key-storage.md and docs/key-derivation.md; the
non-negotiable rules are CLAUDE.md §2 (Golden Rules) and §7 (Security model).
eCash forked from Bitcoin, so every coin that existed before the fork exists on both chains at
the same address — one coin, two ledgers. Spend it carelessly and you can move your BTC along with
your ECX. Splitting fixes that: the wallet sends your eCash to a fresh address of the same
wallet in a transaction Bitcoin will not accept (nLockTime = 499999999 with non-final inputs,
which Bitcoin reads as a lock ~9,500 years out). Afterwards the two chains hold genuinely separate
coins. Only eCash moves; your Bitcoin is untouched and only an eCash fee is paid.
Working out which coins need this takes two steps:
1. Block height — free, automatic. A coin confirmed below the fork height predates the split, so it's shared. This runs on every sync and drives the prompt on the home screen.
2. Asking Bitcoin — on demand. Height alone isn't proof. eCash permits the replay marker but doesn't require it, so an ordinary transaction from any other wallet is valid on both chains, and its outputs land on both at post-fork heights. Height would call those safe when they aren't. Settings → Check for splittable coins resolves it by asking a Bitcoin backend about each coin's exact outpoint: unknown to Bitcoin means chain-specific; present and unspent means still shared; already spent there means a replay would be a double-spend, so it's separated regardless of height.
Coins that height can't vouch for and no check has resolved are reported as unverified rather than safe — the app won't tell you you're separated when it doesn't know. The check is read-only (HTTP requests to a Bitcoin backend; nothing is signed or broadcast), it's a point-in-time answer, and it does reveal your addresses to that backend — which is why it's a button rather than automatic.
CLAUDE.md— architecture bible (the what and why; wins on conflicts).PLAN.md— full build plan + tracked checklist.DESIGN.md— visual spec (tokens, type, components, voice).docs/release.md— build / sign / ship to App Store + Google Play.docs/coin-splitting.md— how shared coins are detected and separated (the money-critical one).docs/— decision records: key storage & derivation, wallet/network model, accounts & labels, backends & endpoints, CoinNews, and more.
eCash.com Wallet stands on these projects. The same list is shown in-app under Settings → About →
Open-source licenses, sourced from a single array (OpenSourceLicense.all in
Sources/ECashWalletMobile/App/OpenSourceLicense.swift) — add or edit a credit there and both the
app screen and this table should be kept in sync.
| Project | Use | License |
|---|---|---|
| Skip | Swift→Kotlin cross-platform toolchain + frameworks | MPL-2.0 |
Bitcoin Dev Kit (bdk-swift / bdk-android) |
Wallet engine | Apache-2.0 / MIT |
| swift-crypto | ed25519 signing (Thunder sidechain) | Apache-2.0 |
| SwiftBlake3 | BLAKE3 hashing (Thunder addresses) | ISC (bundled BLAKE3 C: CC0-1.0 / Apache-2.0) |
| SkipKeychain | Secure mnemonic storage | LGPL-3.0 |
| swift-qrcode-generator | Receive QR codes | MIT |
| SkipQRCode | Send QR scanning (Android camera) | LGPL-3.0 |
| JetBrains Mono | Mono / numeric typeface | OFL-1.1 |
libsodium (ristretto255 subset, vendored in Packages/Ristretto255) |
Thunder sidechain keys & signatures | ISC |
| Satoshi | Display typeface (not in the repo — fetched at build time by scripts/fetch-fonts.sh) |
ITF Free Font License |
| Material Symbols | Icon set (.symbolset) |
Apache-2.0 |
Release note: bundling the full license texts / copyright notices (required by MIT/Apache/OFL) is still TODO — the in-app screen currently links out. See
PLAN.mdMilestone F.
Copyright (C) 2026 LayerTwo Labs and contributors.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. See LICENSE.txt for the full text (SPDX: GPL-2.0-or-later).