Skip to content

Repository files navigation

Solfare — Solana Wallet (Flutter)

A self-built clone of the Solflare wallet, written from scratch in Flutter. Built solo as a deep-dive into wallet security, Solana RPC, and the mobile patterns a self-custody app has to get right.

Where it's strong: the security core. Centralized Keyring primitive for every signing call site, versioned PBKDF2 envelope for passcode storage with silent legacy-plaintext migration (ADR 0002), FlutterSecureStorage explicitly configured with KeychainAccessibility.first_unlock_this_device, intermediate-buffer scrubbing after key derivation, post-send polling fallback for flaky WebSockets, in-flight wallet-switch guards so stale balance responses can't pollute the new account's UI, lifecycle-aware WebSocket with exponential backoff, an iOS-side MethodChannel that paints a privacy overlay on the app-switcher snapshot, a biometric unlock whose Keychain item is bound to biometryCurrentSet so enrolling a new face invalidates it, and a transaction preview that decodes and risk-checks every instruction before anything is signed. 438 tests.

Where it's not: this is a Flutter-only project. There is no advanced native iOS or Android code beyond the screenshot-blocking and deep-link MethodChannels — no Secure Enclave integration, no hardware wallet support, no SSL pinning, no Universal Link or Mobile Wallet Adapter wiring (solfare:// deep links are wired; the OS-verified ones are not). Android release still signs with debug keys. PBKDF2 iteration count is 100k, below the 2025 OWASP recommendation of ~600k (the version envelope is set up for an upgrade). And as a solo project, it has no shipped-to-production scar tissue — only what I learned reverse-engineering Solflare.

A wallet is judged on the parts a user never sees: what a transaction will do before it's signed, which key is in memory and when it's wiped, what the balance says when the socket drops mid wallet-switch. That's where this repo spends its effort, and it's what I'd want to be measured on — designing for the failure case first, writing the design down before the code, and shipping the test that would have caught the bug. The limits above are listed on purpose; I'd rather be read accurately than impressively.

What it does

  • Wallet onboarding — BIP-39 mnemonic generation or import, with confirm-phrase verification and biometric / passcode setup.
  • Multi-wallet — create, rename, switch, and export multiple wallets. Lazy migration from the pre-multi-wallet single-mnemonic storage format.
  • Send & receive SOL — address validation, QR scanning, on-device signing, live balance updates over WebSocket, polling fallback if the WS drops.
  • SPL tokens & NFTs — Helius DAS for both, with local caching that survives cold restart.
  • Swap — Jupiter v2 (/order + /execute) with v0-VersionedTransaction signing on-device.
  • Staking — single bundled createAccount + initialize + delegate transaction so a partial land doesn't leave orphan stake accounts.
  • Market data — CoinGecko-backed prices and charts via a singleton client with serialised queue, in-flight coalescing, and stale-cache fallback on 429.
  • Transaction preview — every instruction decoded against a program registry and run through a risk engine, so the approval sheet says what will actually move rather than showing a blob of base64.
  • Biometric unlock — Face ID releases the wrap key from the Keychain itself; the OS is the gate, not a boolean in Dart.
  • In-app dApp browser with solfare:// deep-link requests and a connected-apps list, address book, l10n, light/dark themes, configurable RPC (Mainnet / Devnet).

Security model

Surface How it's handled
Mnemonics + private keys at rest FlutterSecureStorage (iOS Keychain / Android Keystore) with KeychainAccessibility.first_unlock_this_device
Passcode storage Versioned envelope v1:salt:iter:hash over PBKDF2-HMAC-SHA256; constant-time compare; silent legacy-plaintext migration — see ADR 0002
Key derivation Single Keyring primitive — every signing path goes through one place — see ADR 0001
Intermediate seed + privkey bytes try/finally zeroing inside Keyring; ExportPrivateKeyScreen scrubs displayed key bytes on dispose
Screenshot / app-switcher leak Android FLAG_SECURE; iOS routes through a MethodChannel to a Swift handler that paints a privacy overlay on willResignActive
Clipboard SecureClipboard.copySensitive auto-clears after 30s only if the value hasn't changed
Biometric unlock Wrap key stored under KeychainAccessibility.passcode + AccessControlFlag.biometryCurrentSet — the OS releases it only after authenticating the holder, and a newly enrolled face or finger invalidates it
Brute-force 5-attempt lockout in PasscodeBloc with backoff
Fresh-install hygiene _wipeSecureStorageOnFreshInstall clears stranded Keychain entries on first run after install

Architecture

Feature-sliced Clean Architecture. wallet is the canonical example with full data / domain / presentation layering; swap and staking are flatter because they're thinner features and the layering hadn't earned its keep there yet — that inconsistency is honest, not aspirational.

lib/
├── core/
│   ├── network/        # http_retry, coingecko_client
│   ├── security/       # passcode_crypto, secure_store, secure_screen, secure_clipboard
│   ├── wallet/         # keyring (single signing primitive), active_wallet
│   └── ...
├── features/
│   ├── wallet/         # data / domain / presentation (full Clean layers)
│   ├── swap/           # data / presentation (flatter)
│   ├── staking/        # presentation only
│   ├── market/ ...
├── shared/             # splash, onboarding, reusable widgets
└── l10n/
ios/Runner/
└── AppDelegate.swift   # MethodChannels: app-switcher privacy overlay, solfare:// deep links
test/
├── core/security/      # passcode envelope + migration, mnemonic envelope, biometric lock, app lock
├── core/wallet/        # keyring (BIP-44 determinism, invariants, secret parsing)
├── core/solana/        # instruction decoder, risk engine, recipient check, dApp sessions, Solana Pay
├── core/network/       # http_retry (retry-then-succeed, timeout, max-attempts), friendly error mapping
├── core/currency/      # money formatting across 20 currencies
└── features/           # wallet, swap, market, staking, settings, homepage
docs/
├── adr/                # architecture decision records
└── learning/           # personal build-journal markdowns (gitignored from main tracking)

438 tests across 43 files, from the passcode envelope out to swap pair selection and the swap screen's height budget. Storage tests fake FlutterSecureStorage via TestDefaultBinaryMessenger so they run without a device or emulator. Run with flutter test.

Tech stack

Flutter / Dart, flutter_bloc, go_router, solana, bip39, ed25519_hd_key, bs58, crypto (PBKDF2-HMAC-SHA256), flutter_secure_storage, flutter_windowmanager, http, web_socket_channel, webview_flutter, fl_chart, lottie, flutter_svg, qr_flutter, qr_code_dart_scan. Custom FKGrotesk typography.

Running locally

flutter pub get
flutter run \
  --dart-define=HELIUS_API_KEY=your_helius_key \
  --dart-define=JUPITER_API_KEY=your_jupiter_key

API keys are passed per build rather than bundled. .env used to be listed under flutter: assets:, which put it inside the IPA and the APK as a plain file — unzip -p Solfare.ipa 'Payload/*/flutter_assets/.env' returned the key from any user's copy. .gitignore kept it out of the repository, which is correct and also beside the point.

A --dart-define is compiled into the binary and still extractable by anyone determined enough; nothing shipped to a device is a secret. What it buys is a key that is per-build, so it can be rotated and scoped without a code change, and a debug key that cannot silently become the production one. Without them the app runs and Helius-backed features (tokens, NFTs, live balances) come back empty.

Mainnet by default; Devnet selectable from Settings → Network.

To run the test suite:

flutter test

Build journey

I've been documenting this build on X — short demo clips:

Longer-form build journal lives at github.com/frankolien/solflare-clone-guide.

About me

Flutter developer focused on mobile apps that touch crypto, security, and real-time data. Open to roles — Frankolien123@gmail.com · @frank_olien123.

About

A self-built clone of the Solflare wallet, written from scratch in Flutter.

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages