AndroidCryptoWallet is a non-custodial Android wallet for Bitcoin and several EVM chains. You create or restore a wallet from a BIP-39 seed; keys stay on the device. Bitcoin uses native SegWit. Ethereum, BSC, Polygon, Arbitrum, Base, Optimism, and Avalanche share one EVM stack that is easy to extend with another curated chain.
Disclaimer. This software has not been audited. Use testnets first. Never share a seed phrase. You are responsible for funds you put on mainnet.
- Non-custodial; seed encrypted with Android Keystore + EncryptedSharedPreferences
- Bitcoin native SegWit (BIP-84), testnet4 + mainnet
- EVM: Ethereum, BSC, Polygon, Arbitrum, Base, Optimism, Avalanche
- Compose + Hilt + Room + BDK + Web3j
- Single-address Bitcoin model (change returns to receive index 0)
- No ERC-20 / NFT portfolio yet
- No hardware wallets
- Not audited
- Not on Play yet
Keys never leave the device. The secret at rest is the seed (BIP-39 mnemonic plus optional passphrase), stored in EncryptedSharedPreferences (bitcoin_mnemonic / ethereum_mnemonic) under an Android Keystore MasterKey (AES-256). Android backup is disabled (allowBackup="false"). backup_rules.xml and data_extraction_rules.xml also exclude those prefs files, Room (wallet.db), and the rest of app data so cloud backup and device-to-device transfer cannot copy the seed. Public data (receive address, network, derivation index) lives in Room. Secrets never go in Room, DataStore, or logs.
Signing does not persist private keys. On send, the app decrypts the seed, rebuilds keys in memory, signs, and drops the seed. Watch-only Bitcoin wallets have no seed and cannot send. Logcat does not record the seed, passphrase, or signed raw transaction hex. OkHttp BODY logging is not used; the logging interceptor is debugImplementation only.
HTTPS is required (usesCleartextTraffic="false" plus a network security config). RPC and explorer endpoints are not authenticated and are not certificate-pinned — Settings lets you point each provider at your own URL. A malicious or compromised node can lie about balances, fees, and history, or refuse to broadcast; it never receives the seed. Prefer a node you run or trust, especially on mainnet.
This project has not been audited. Report vulnerabilities privately — see SECURITY.md. Do not open a public GitHub issue for security findings.
The app talks to third-party HTTPS hosts for chain data and USD prices. Defaults (overridable in Settings, except CoinGecko):
| Purpose | Default hosts |
|---|---|
| Bitcoin (Esplora) | mempool.space (/api/, /testnet4/api/) |
| USD prices | api.coingecko.com |
| Ethereum RPC | ethereum.publicnode.com, ethereum-sepolia-rpc.publicnode.com |
| Ethereum explorer | eth.blockscout.com, eth-sepolia.blockscout.com |
| BSC RPC | bsc-dataseed.bnbchain.org, data-seed-prebsc-1-s1.bnbchain.org |
| BSC explorer | api.bscscan.com, api-testnet.bscscan.com |
| Polygon RPC | polygon-bor-rpc.publicnode.com, polygon-amoy-bor-rpc.publicnode.com |
| Polygon explorer | api.polygonscan.com, api-amoy.polygonscan.com |
| Arbitrum RPC | arb1.arbitrum.io, sepolia-rollup.arbitrum.io |
| Arbitrum explorer | api.arbiscan.io, api-sepolia.arbiscan.io |
| Base RPC | mainnet.base.org, sepolia.base.org |
| Base explorer | api.basescan.org, api-sepolia.basescan.org |
| Optimism RPC | mainnet.optimism.io, sepolia.optimism.io |
| Optimism explorer | api-optimistic.etherscan.io, api-sepolia-optimistic.etherscan.io |
| Avalanche RPC | api.avax.network, api.avax-test.network |
| Avalanche explorer | api.snowtrace.io, api-testnet.snowtrace.io |
Cleartext http:// is blocked. Custom provider URLs in Settings must be https://. There is no API key on these public endpoints; operators can see the addresses you query.
Bitcoin — native SegWit on mainnet and testnet4, including watch-only addresses.
EVM — one wallet UI per family; pick the network from a dropdown. New EVM families are curated by the developer (users cannot add arbitrary RPCs yet).
| Family | Mainnet | Testnet |
|---|---|---|
| Ethereum | 1 |
Sepolia 11155111 |
| BSC | 56 |
BSC Testnet 97 |
| Polygon | 137 |
Amoy 80002 |
| Arbitrum | 42161 |
Sepolia 421614 |
| Base | 8453 |
Sepolia 84532 |
| Optimism | 10 |
Sepolia 11155420 |
| Avalanche | 43114 |
Fuji 43113 |
- Copy
local.properties.exampletolocal.properties(Android Studio also creates this withsdk.dir). - Optionally set mock watch-only addresses (
MOCK_BITCOIN_WALLET_TESTNET4/MOCK_BITCOIN_WALLET_MAINNET) to any bitcoin address you want to view in the wallet lists and tx lists. - Open the project in Android Studio and run the
debugbuild (Bitcoin testnet4), or from the repo root:
./gradlew :app:assembleDebugSee CONTRIBUTING.md for tests, PR expectations, and emulator-data warnings. Do not uninstall the debug app or run instrumented tests against an emulator that already has wallets — those flows wipe Room and the encrypted mnemonic store.
Compose UI, Hilt, Room, BDK (Bitcoin), and Web3j (EVM). Bitcoin chain data comes from mempool.space. EVM balance/send uses JSON-RPC; tx history uses Blockscout or Etherscan-compatible explorers.
Bitcoin: keys, seed, and signing (BIP-39 / BIP-32 / BIP-84)
Supported BIPs:
- BIP-39 — 12-word English mnemonic, optional passphrase
- BIP-32 — HD derivation (via BDK)
- BIP-84 — Native SegWit (
bc1q/tb1q) at accountm/84'/0'/0'(mainnet) orm/84'/1'/0'(testnet4)
This is an HD wallet. The encrypted secret at rest is the seed (mnemonic + optional passphrase), not a per-address WIF or xprv. Public data (receive address, network, derivation index) lives in Room. Secrets never go in Room, DataStore, or logs.
At rest, the seed is stored in EncryptedSharedPreferences keyed by wallet id, with an Android Keystore MasterKey (AES-256). Backup is disabled (allowBackup="false").
Signing does not persist private keys. On send, the app decrypts the seed, BDK rebuilds an in-memory BIP-84 wallet, derives the key for the receive address, signs the PSBT, and drops the seed from the call stack. Watch-only wallets have no seed and cannot send.
Bitcoin: balance and UTXOs
The wallet is a single-address model: balance, history, and coins all belong to the BIP-84 receive address (external index 0). Change from a send is paid back to that same address, so it stays visible and spendable on the next send.
The amount shown on wallet details is the address chain balance from the RPC provider (confirmed received minus confirmed spent). Incoming or outgoing mempool amounts are shown separately as unconfirmed; they are not added to the spendable total.
Spending does not use that cached display figure. Right before building a transaction the app fetches UTXOs for the receive address and keeps only confirmed outputs (status.confirmed == true). Unconfirmed coins cannot be selected. Those confirmed UTXOs are the spendable balance: their satoshi values are what BDK coin-selects against the send amount plus fee. Fee is sum(inputs) − sum(outputs) at the chosen sat/vB rate; if confirmed UTXOs cannot cover amount plus fee, send fails with insufficient funds.
Bitcoin RPC provider
Chain data comes from mempool.space (Esplora-compatible HTTP: address balance, history, UTXOs, and broadcast). Mainnet uses https://mempool.space/api/; testnet4 uses https://mempool.space/testnet4/api/. No API key is required for low request quantities.
To add another chain client later:
- Implement every method on
BitcoinRemoteDataSource(balance, transactions, UTXOs, transaction hex, broadcast, tip height). Do not ship a partial client. - Add a
@Binds(or@Provides) of that type in Hilt and swap the mempool.space bind inDataBindsModule. - Put provider-specific URLs and keys in
local.properties→ BuildConfig, never in git. - Reuse the existing
MsApiFactorypattern (per-network Retrofit cache) if the API is HTTP.
EVM stack
AndroidCryptoWallet supports easily adding new EVM chains, but curated by the developer. In the future we might add the feature for the user to add chains.
Each family (Ethereum, BSC, …) is a chain-select entry. Users pick a network inside that family (Sepolia, BSC Testnet, …) from the dropdown. You do not add new screen packages or repositories. All families share the same wallet screens and backend (JSON-RPC, signing, tx history). BSC is the reference implementation.
What you get for free:
- JSON-RPC balance/send and EIP-1559 signing (
JsonRpcEvmRemoteDataSource,Web3jEvmKeyEngine) - BIP-44 coin type
60'(MetaMask-compatible addresses) - Room
ethereum_wallet/ tx cache and encryptedethereum_mnemonicprefs (names unchanged) - Shared screens under
app/src/main/java/.../ui/evm/**, filtered byEvmFamily - Receive QR: EIP-681
ethereum:address@chainId - Amount labels from
network.nativeSymbol
How to add a curated EVM family. All families share the same wallet screens and backend. BSC is the reference implementation.
| Task | File |
|---|---|
| Add enum value | app/src/main/java/network/bahn/androidcryptowallet/domain/model/EvmFamily.kt |
| Add networks (testnet + mainnet typical) | app/src/main/java/network/bahn/androidcryptowallet/domain/model/EvmNetwork.kt |
Each EvmNetwork needs: family, label (dropdown), chainId, nativeSymbol. EvmNetwork.networksFor(family) filters automatically.
Wallets store network as the enum name (SEPOLIA, BSC_TESTNET, …). New constants are additive only — no migration. Do not rename existing enum names.
Tests: extend app/src/test/java/.../domain/model/EvmNetworkTest.kt.
| Task | File |
|---|---|
| RPC URL per network | app/src/main/java/network/bahn/androidcryptowallet/data/repository/DefaultProviderCatalog.kt |
| Explorer URL + kind per network | same |
rpcUrls = mapOf(EvmNetwork.NEW_TESTNET to "https://…", …)
explorerEndpoints = mapOf(
EvmNetwork.NEW_TESTNET to EvmExplorerEndpoint(
baseUrl = "https://…",
kind = EvmExplorerKind.BLOCKSCOUT, // or ETHERSCAN
),
)Tx history adapters (picked automatically by RoutingEvmTransactionRemoteDataSource):
| Kind | API | Class |
|---|---|---|
BLOCKSCOUT |
Blockscout REST v2 | BlockscoutEvmTransactionRemoteDataSource |
ETHERSCAN |
Etherscan-compatible txlist |
EtherscanEvmTransactionRemoteDataSource |
If the explorer is neither format, add a new EvmExplorerKind, adapter, and routing branch.
Tests: extend app/src/test/java/.../data/remote/evm/EvmChainCatalogTest.kt.
| Task | File |
|---|---|
| Chain-select enum + order | app/src/main/java/.../ui/chain/SupportedChain.kt |
| Label + icon on chain select | app/src/main/java/.../ui/chain/ChainSelectScreen.kt |
| List title, default wallet name, clipboard label | app/src/main/java/.../ui/chain/EvmFamilyUi.kt |
SupportedChain → EvmFamily |
EvmFamilyUi.kt → toEvmFamily() |
Add to app/src/main/res/values/strings.xml (mirror Ethereum/BSC):
| Resource | Example (BSC) |
|---|---|
chain_* |
chain_bsc |
*_wallets_title |
bsc_wallets_title |
*_wallet_list_item_label |
bsc_wallet_list_item_label |
receive_clipboard_label_* |
receive_clipboard_label_bsc |
Add app/src/main/res/drawable/ic_chain_<family>.xml.
In app/src/main/java/.../ui/navigation/WalletNavHost.kt:
SupportedChain.NEW_FAMILY ->
navController.navigate(EvmWalletListRoute(EvmFamily.NEW_FAMILY))Create/restore/details/send/receive/edit reuse the existing EVM graph — no new route types.
In app/src/main/java/.../data/local/prefs/SelectedEvmNetworkDataStore.kt → defaultNetwork(): set the family’s testnet (e.g. ETH → SEPOLIA, BSC → BSC_TESTNET).
./gradlew :app:testDebugUnitTestManual smoke (do not clear emulator app data):
- Chain select → new family → dropdown shows only that family’s networks.
- Create wallet on testnet → receive QR ends with
@<chainId>. - Send native on testnet.
- Details → tx history loads (or empty, no crash).
- Other families unchanged.
- Add new screen packages — reuse
ui/evm/** - Merge EVM with the Bitcoin stack
- Rename/drop
ethereum_*tables orethereum_mnemonicprefs - Run destructive migrations or reinstall/clear app data to test
- Add ERC-20 / BEP-20 tokens in the same change
| Area | Files |
|---|---|
| Domain | EvmFamily.kt, EvmNetwork.kt |
| Catalog | DefaultProviderCatalog.kt |
| Tx history | EtherscanTx.kt, EtherscanEvmTransactionRemoteDataSource.kt, RoutingEvmTransactionRemoteDataSource.kt |
| Chain select | SupportedChain.kt, ChainSelectScreen.kt, EvmFamilyUi.kt, strings.xml, ic_chain_bsc.xml |
| Nav | WalletNavHost.kt |
| Defaults | SelectedEvmNetworkDataStore.kt |
| Tests | EvmNetworkTest.kt, EvmChainCatalogTest.kt, EtherscanTxMappingTest.kt |
See CONTRIBUTING.md for how to build, which tests to run, and PR expectations. Please follow the Code of Conduct.
This is a wallet. Report vulnerabilities privately — see SECURITY.md. Do not open a public GitHub issue for security findings.
Released changes are listed in CHANGELOG.md.
MIT — see LICENSE.
This software has not been audited. Use testnets first. Never share a seed phrase. You are responsible for funds you put on mainnet.

