Non-Custodial Mobile Crypto Wallet Development
Non-custodial wallet means one thing: the user's private key never leaves their device. No server database with keys, no "support recovery," no central point of compromise. This is fundamental architectural decision that defines the entire development stack.
BIP39/32/44 — Standards You Can't Ignore
Start with mnemonic phrase. BIP39 defines list of 2048 words and algorithm generating 512-bit seed from 12 or 24 words + optional passphrase via PBKDF2-HMAC-SHA512 (2048 iterations). Seed → master key via BIP32 HMAC-SHA512. Hierarchical key derivation by BIP44 path: m/44'/60'/0'/0/0 — first Ethereum address, m/44'/0'/0'/0/0 — first Bitcoin address.
Why this matters: user should be able to recover all their addresses in any compatible wallet (MetaMask, Trust Wallet, Ledger) from single mnemonic. Deviating from standard strips user of this ability.
Generating Mnemonic on Android:
// BitcoinJ or Web3j for BIP39
val entropy = ByteArray(16) // 128 bits → 12 words
SecureRandom().nextBytes(entropy)
val mnemonic = MnemonicCode.INSTANCE.toMnemonic(entropy)
// ["word1", "word2", ..., "word12"]
// seed from mnemonic
val seed = MnemonicCode.toSeed(mnemonic, "") // without passphrase
val masterKey = HDKeyDerivation.createMasterPrivateKey(seed)
Mnemonic checksum (last word or part of it) — mandatory validation on import. Users regularly mistype.
iOS, Swift:
// WalletCore from Trust Wallet — excellent library for iOS/Android
let wallet = HDWallet(strength: 128, passphrase: "")
let mnemonic = wallet.mnemonic // 12 words
let ethAddress = wallet.getAddressForCoin(coin: .ethereum)
Trust Wallet Core (WalletCore) — open source, supports 60+ blockchains, implements all BIP standards. Used in Trust Wallet, Argent, and dozens of other wallets. For new non-custodial wallet — standard foundation choice.
Secure Seed and Private Key Storage
Key cannot be stored in plaintext anywhere: not in files, not SharedPreferences, not database. Scheme:
- User creates PIN or sets up biometry
- Generate random encryption key (AES-256), protected by Android Keystore / iOS Secure Enclave with biometry binding
- Encrypt seed with this key
- Store encrypted blob in encrypted DB (SQLCipher) or EncryptedSharedPreferences
On biometric authentication:
val cryptoObject = BiometricPrompt.CryptoObject(cipher) // cipher bound to Keystore key
biometricPrompt.authenticate(promptInfo, cryptoObject)
// in onAuthenticationSucceeded:
val decryptedSeed = result.cryptoObject?.cipher?.doFinal(encryptedSeed)
Private key in decrypted form lives in memory only during transaction signing. Afterward — zero array bytes, GC doesn't guarantee freeing, so explicit Arrays.fill(keyBytes, 0.toByte()).
Working with Blockchain
For Ethereum-compatible networks (ETH, BSC, Polygon, Arbitrum, Optimism) — Web3j (Android) or web3.swift (iOS). For Bitcoin — BitcoinJ. For Solana — Solana Mobile Stack SDK. For TON — ton-kotlin or TonConnect.
Network connection via RPC provider: Infura, Alchemy, QuickNode. For privacy — own node (expensive to maintain) or multiple providers with fallback.
Sending ETH Transaction:
val credentials = Credentials.create(privateKeyHex)
val nonce = web3j.ethGetTransactionCount(
credentials.address,
DefaultBlockParameterName.PENDING
).send().transactionCount
val rawTransaction = RawTransaction.createEtherTransaction(
nonce,
gasPrice,
gasLimit,
toAddress,
amountInWei
)
val signedTransaction = TransactionEncoder.signMessage(rawTransaction, chainId, credentials)
val txHash = web3j.ethSendRawTransaction(
Numeric.toHexString(signedTransaction)
).send().transactionHash
Private key used only for TransactionEncoder.signMessage — signing happens locally, only signed transaction without key goes to network.
EIP-1559 and Gas Calculation
Since London hardfork (EIP-1559), transactions have maxFeePerGas and maxPriorityFeePerGas instead of simple gasPrice. Correct calculation: eth_feeHistory RPC method for analyzing recent blocks, algorithm for maxPriorityFeePerGas (tip) based on percentiles. MetaMask uses 50th percentile of priority fees from last 5 blocks as baseline advice.
Show user three options (slow/normal/fast) with estimated confirmation time — standard UX.
WalletConnect for DApps
Without WalletConnect, wallet isolated from DeFi ecosystem. WalletConnect v2 (Sign API) — protocol connecting wallet and dApp via relay server. Implementation: WalletConnect Swift SDK (iOS), WalletConnect Kotlin SDK (Android).
Session established via QR code or deep link:
- dApp generates URI:
wc:...@2?relay-protocol=irn&symKey=... - User scans QR in wallet
- E2E-encrypted session established via relay
- dApp requests
eth_sendTransaction→ user sees details → signs
Seed Phrase Backup Flow
Seed backup UX — critical part. Users lose funds from seed loss. Correct flow:
- Show mnemonic — request confirmation that they wrote it down
- Verification: show 3 random words from phrase, ask for ordinal numbers
- Remind about backup in onboarding and periodically
Prevent screenshots on seed screen via FLAG_SECURE / iOS UIScreen.capturedDidChangeNotification.
Multichain and Tokens
ERC-20 tokens don't need separate keys — same Ethereum address. Balance via balanceOf(address) contract call. Token list — via CoinGecko API or Trust Wallet Assets repo (open list with icons for 10000+ tokens).
NFT (ERC-721, ERC-1155) — ownerOf(tokenId) / balanceOf(address, id). Metadata via tokenURI → IPFS or HTTP.
Compliance Requirements
Non-custodial wallet in most jurisdictions doesn't require license — user manages their own keys. But adding currency exchange (swap), fiat on-ramp changes situation. Legal consultation on regulations mandatory before launch with such features.
Timeline for developing basic non-custodial wallet (Ethereum + ERC-20 tokens + send/receive + WalletConnect + seed backup) — 2–4 months depending on team and supported networks. Adding each new blockchain with native integration — 2–4 weeks additionally. Cost calculated after detailed functional requirements.







