A self-custody, multi-chain crypto wallet by ChainLens — one codebase shipping as a desktop app (Electron, Windows/macOS/Linux), Chrome browser extension (Manifest V3), and Android app (Capacitor).
It manages 22 default mainnet networks from a single seed phrase (plus any custom EVM network you add yourself), adds focused Privacy Mode and Testnet Mode network sets, connects to dApps (EVM, Solana, and Cardano), swaps tokens same-chain and cross-chain, doubles as your system default browser on Windows and Android, and never lets your keys leave your device.
Ecosystems: EVM · Solana · Cardano · Bitcoin · Polkadot · Tron · Dogecoin · Monero · Zcash · Midnight
- One seed, 22 default chains — BIP-39/32/44 derivation for EVM, Solana, Cardano, Bitcoin, Polkadot, Tron, and Dogecoin, all in one portfolio with a unified USD total.
- Custom networks — add any EVM-compatible chain (name, RPC URL, chain ID, symbol, explorer) from the + button next to the account switcher. The RPC is verified via
eth_chainIdbefore the chain is saved, and custom chains get balances, sends, and dApp chain-switching like built-ins (desktop). - Portfolio search — a search bar on the Dashboard filters networks, tokens, or collectibles in whichever portfolio tab is active.
- Privacy Mode — a focused portfolio for Monero (XMR), Zcash transparent (ZEC), and Midnight (NIGHT), derived lazily from the same mnemonic and kept mutually exclusive with Testnet Mode.
- Testnet Mode — flips the wallet to Sepolia / devnet / preprod / Bitcoin testnet / Shasta networks for no-real-funds testing, with testnet-safe address substitution where encodings differ.
- Truly self-custody — the mnemonic is encrypted at rest and private keys exist only transiently in the privileged process during signing. The UI layer never sees them.
- No API keys to configure — keyed providers are proxied through a hosted Cloudflare Worker, so the app works out of the box with zero secrets shipped in the bundle.
- dApp ready — injects
window.ethereum(EIP-1193 / EIP-6963),window.solana(Wallet Standard),window.cardano.magicmoney(CIP-30), andwindow.midnight(Midnight DApp Connector), plus the VESPR-authorizedwindow.cardano.vesprand Lace-authorizedwindow.midnight.mnLacecompatibility keys and WalletConnect v2. - Built-in swaps — same-chain DEX aggregation and cross-chain bridging/exchange in one Swap page.
- Multi-account — BIP-44 account-index switcher (accounts 0–9) from the Portfolio header.
- Abstract Global Wallet — surfaces your AGW smart account inside the same portfolio total.
- Platform-native unlocks — Windows Hello, Touch ID, and Android biometrics can unlock an enrolled wallet while the password remains the backup.
- Tor Mode for the built-in browser — desktop downloads, verifies, and starts a private Tor runtime on first use; Android bundles Tor Android directly. Both verify the exit with the Tor Project and fail closed when Tor is unavailable; Android also verifies v3 onion access before reporting ready.
- One-click updates — a Software Update button in Settings pulls new versions straight from GitHub Releases (Windows/Linux apply silently; macOS opens the download; Android opens the newest APK page). The extension also has a side-panel mode (Phantom-style dock).
- Usable as your default browser — Settings can register MagicMoney as the system default browser on Windows and Android, so every link you click anywhere opens in the wallet's private dApp browser (with optional Tor Mode). Links clicked inside the wallet UI open in-app too — the wallet never bounces you out to Chrome/Brave/Edge.
- Native downloads — files downloaded in the built-in browser (including NFT image saves) are handled natively with a slim progress bar across the top of the window, on both desktop and Android.
- Runs in the background — closing the desktop wallet minimizes it to the system tray instead of quitting. The tray menu can open the Wallet or the Browser independently, and Exit MagicMoney actually quits.
| Network | Ecosystem | Native | Primary provider |
|---|---|---|---|
| Ethereum | EVM L1 | ETH | Alchemy |
| Arbitrum One | EVM L2 | ETH | Alchemy |
| Optimism | EVM L2 | ETH | Alchemy |
| Base | EVM L2 | ETH | Alchemy |
| Polygon | EVM L2 | POL | Alchemy |
| Avalanche | EVM L1 | AVAX | Alchemy |
| Blast | EVM L2 | ETH | Alchemy |
| Gnosis | EVM L1 | xDAI | Alchemy |
| Abstract | EVM L2 | ETH | Alchemy |
| ApeChain | EVM L2 | APE | Alchemy |
| Ronin | EVM L2 | RON | Alchemy |
| Soneium | EVM L2 | ETH | Alchemy |
| WorldChain | EVM L2 | WLD | Alchemy |
| Zora | EVM L2 | ETH | Alchemy |
| Monad | EVM L1 | MON | monad.xyz RPC (+ rotation) |
| HyperEVM | EVM L1 | HYPE | hyperliquid.xyz RPC |
| Solana | SVM | SOL | Helius |
| Cardano | eUTXO | ADA | Blockfrost (+ Koios fallback) |
| Bitcoin | UTXO | BTC | Tatum (+ Esplora fallback) |
| Polkadot | Substrate | DOT | Tatum |
| Tron | TRON | TRX | TRON HTTP API (+ TronGrid fallback) |
| Dogecoin | UTXO | DOGE | BlockCypher |
Every chain has a multi-RPC fallback chain: the primary endpoint is always tried first, and keyless public mirrors only engage on transport errors, so the normal path is unchanged.
Any EVM-compatible chain can be added from the + button beside the account switcher on the desktop Dashboard: name, RPC URL, chain ID, currency symbol, and optional block-explorer URL. The RPC endpoint is verified live (eth_chainId must match the entered chain ID) before the network is saved. Custom chains show up as normal portfolio cards with native balances, support sending, and participate in dApp wallet_switchEthereumChain / wallet_addEthereumChain routing. They are stored per-device and can be removed from their chain card.
Privacy Mode is a filtered mainnet portfolio, not a testnet substitute. It shows only privacy-focused chains, derives their addresses the first time the mode is enabled, and turns off Testnet Mode automatically.
| Network | Native | Notes |
|---|---|---|
| Monero | XMR | Full Monero address + private view key derived from the seed; balance scanning uses keyless remote nodes with a restore-height wallet birthday. |
| Zcash | ZEC | Transparent t1... address, balance, fee estimate, and sends. Shielded receivers are intentionally withheld until shielded scanning is supported. |
| Midnight | NIGHT | Lace-compatible unshielded and shielded address derivation. Receive/balance support is present; sends wait on DUST/proof-server support. |
Swaps are disabled while Privacy Mode is on because the swap providers do not support these routes, and routing privacy assets through a hosted swap provider would undercut the point of the mode.
Testnet Mode swaps the active network set for no-real-funds testing. Prices, NFT floors, swaps, and AGW are disabled or hidden where testnet data would be misleading.
| Mainnet slot | Testnet used |
|---|---|
| Ethereum | Sepolia |
| Arbitrum | Sepolia |
| Optimism | OP Sepolia |
| Base | Sepolia |
| Polygon | Amoy |
| Avalanche | Fuji |
| Blast | Sepolia |
| Gnosis | Chiado |
| Monad | Monad Testnet |
| Abstract | Abstract Testnet |
| ApeChain | Curtis |
| Ronin | Saigon |
| Soneium | Minato |
| WorldChain | Sepolia |
| Zora | Sepolia |
| HyperEVM | HyperEVM Testnet |
| Solana | Devnet |
| Cardano | Preprod |
| Bitcoin | Testnet3 + Testnet4 |
| Tron | Shasta |
Polkadot and Dogecoin stay hidden in Testnet Mode until reliable testnet data providers are wired in.
Download the latest installer from GitHub Releases:
- Windows —
MagicMoney-Wallet-Setup-x.x.x.exe - macOS —
MagicMoney-Wallet-x.x.x.dmg - Linux —
MagicMoney-Wallet-x.x.x.AppImage
Staying up to date. The app checks for updates on launch, and Settings → Software Update is a one-click button that adapts through the whole flow — Check for Updates → Downloading NN% → Restart to Update:
- Windows / Linux (AppImage) — the update downloads and the app relaunches into the new version. No reinstall, no hunting for a download.
- macOS — the button detects the new version and opens the Releases page for a quick drag-install. (Silent macOS auto-apply requires an Apple Developer ID cert + notarization; see Build & Release.)
Shipping a new version to everyone is just a release (below) — users get it from the button.
- Download
magicmoney-extension-vx.x.x.zipfrom GitHub Releases - Unzip it
- Open
chrome://extensions→ enable Developer mode → Load unpacked → select the unzipped folder - Pin the extension from the toolbar
Chrome Web Store submission is planned for the first public release.
Download magicmoney-android-vX.Y.Z.apk from GitHub Releases, allow "install unknown apps" for your browser/file manager, and open the APK.
Android releases are update-over-install compatible as long as they are signed with the same release key. The in-app Software Update row checks GitHub Releases and opens the newest APK download page. Google Play distribution is planned; APK sideloading is the current public path.
Prerequisites: Node.js 20+, npm 10+
npm install
# Desktop (Electron) with hot reload
npm run dev
# Chrome extension build → dist-extension/
npm run build:extension
# then load dist-extension/ as an unpacked extension in chrome://extensions
# Android web bundle + native sync
npm run build:capacitor
# deploy to a connected Android device or emulator
npm run androidUseful checks:
npm run typecheck # tsc on node + web + extension + capacitor tsconfigs
npm test # vitest (wallet-core, crypto-vault, tx-describe, secure-store)MagicMoney is one codebase with four runtime surfaces. All wallet logic — key derivation, balance/token/NFT fetching, transaction building, swap routing, dApp request handling — is shared. Only the platform primitives differ, and they're swapped at build time via Vite aliases:
| Concern | Desktop (Electron) | Extension (Chrome MV3) | Android (Capacitor) | iOS (Capacitor) |
|---|---|---|---|---|
| Privileged runtime | Main process (ipc-handlers.ts) |
Service worker (background.ts) |
In-process wallet router (wallet-local.ts) |
Same as Android |
| Encrypted key storage | safeStorage (OS keychain) |
chrome.storage.local + AES-256 (crypto-vault.ts) |
Capacitor Preferences + AES-256 vault (capacitor-store.ts) |
Same as Android |
| Storage adapter | secure-store.ts |
chrome-store.ts |
capacitor-store.ts |
capacitor-store.ts |
| Renderer ↔ core bridge | contextBridge / IPC |
chrome.runtime.sendMessage (bridge.ts) |
Local bridge (platform-capacitor.ts, wallet-local.ts) |
Same as Android |
| dApp browser | Built-in pop-out WebContentsView |
The user's own browser tabs | Native DappBrowser plugin with separate WebViews |
WKWebView port of the same plugin |
| Native security | Windows Hello / Touch ID | Password unlock | Android biometric unlock, app foreground re-lock, hardware back handling | Face ID / Touch ID (Secure Enclave), foreground re-lock |
The two native targets are one target with two alias sets: both build from src/capacitor/* through the shared recipe in vite.capacitor.shared.ts. src/ios/ contains only an entry point, a stylesheet, and the few modules iOS must replace.
iOS distribution — sideload, not the App Store. MagicMoney is self-distributed from GitHub Releases and the ChainLens site on every platform, iOS included. The release asset is an unsigned .ipa; you install it with Sideloadly or AltStore using your own free Apple ID — no Apple Developer Program membership on either side. Apple's free provisioning means a sideloaded app expires after 7 days and must be re-signed from a computer, and you can have at most 3 sideloaded apps active. In exchange, the iOS build is subject to no store review: the self-updater and the ad-blocker are both present, and nothing is cut to satisfy a guideline. (Tor is absent on iOS for a technical reason, not a policy one — see the gaps table below.)
iOS platform gaps (Apple provides no API for these — they are not TODOs):
| Feature | iOS status |
|---|---|
| Set as default browser | Unavailable — no third-party API to request or query the role |
| Install site as home-screen app | Unavailable — a Safari-only privilege |
| Chromium password import | Stubbed, as on Android. CSV import works |
| Monero send | Receive-only, as on Android and the extension |
| Screenshot blocking | Approximated (no FLAG_SECURE equivalent) — see below |
| Tor Mode | Not available — WKWebView has no proxy override. The toggle is hidden rather than shown permanently disabled |
| Ad-block block counts | Not available — WKContentRuleList never reports a match, so per-page/per-tab counters read 0. Blocking itself is fully active (~109k network rules + ~24k cosmetic) |
All keyed providers (Alchemy, Helius, Blockfrost, Tatum, Moralis, OpenSea, the swap aggregators, and Supabase) route through a Cloudflare Worker (cloudflare-worker/). The client is proxy-first: src/main/api-proxy.ts rewrites provider URLs/headers to the Worker, which injects the real secrets server-side. As a result no provider API keys ship in the app bundle.
The Worker (read.js, swap-proxy.js, lib.js, db.js, auth.js) also adds a shared cross-user KV cache (token metadata, NFT floors), per-IP rate limiting, and EVM-signature-gated write routes for profile sync.
Deliberately kept client-side and direct: keyless, per-IP endpoints — CoinGecko (prices/sparklines), DexScreener, DefiLlama, mempool.space, Magic Eden. Proxying these would collapse every user onto one Worker IP and trigger provider rate limits.
The app ships pointing at a hosted Worker, so it works with no setup. Advanced users can self-host the Worker (see
cloudflare-worker/README.md) or supply their own keys in Settings — config keys default to empty and are stored on-device only.
Renderer / Popup / WebView (React) Privileged runtime
────────────────────────── ──────────────────────────────────
UI, balances, addresses ←── Cloudflare Worker → Alchemy / Helius / …
window.wallet.getBalances() ──► IPC / message / local router → balance-fetcher.ts
window.wallet.generate() ──► wallet-core.ts → returns word[] only
window.wallet.confirmBackup() ──► wallet-core.ts → derive → encrypted storage
dApp page (injected provider) Privileged runtime (signing)
────────────────────────── ──────────────────────────────────
window.ethereum (EIP-1193/6963) ──► per-origin approval → tx-sender.ts
window.solana (Wallet Standard) ──► sign with ed25519 (@noble) → broadcast
window.cardano.magicmoney / vespr──► cardano-cip30.ts → witness → submit
WalletConnect v2 URI ──► @walletconnect/sign-client
Guarantees:
- Mnemonic encrypted at rest — OS keychain (
safeStorage) on desktop, AES-256 behind a user password in the extension and Android app. - Private keys are derived transiently for signing and never persisted in the clear; the renderer/popup only ever receives public addresses and balance strings.
- Desktop hardening:
contextIsolation: true,sandbox: true,nodeIntegration: false. - The extension's
MAIN-world inject script provides the dApp APIs but has no access to wallet storage — every privileged action crosses the message bridge. - Android dApp pages run in separate native WebViews from the trusted wallet UI, with provider requests validated and routed back through the same approval flow.
MagicMoney is a fully-fledged signer for EVM, Solana, and Cardano dApps, and connects to Midnight dApps.
-
EVM — injects
window.ethereumwith EIP-1193 + EIP-6963 (multi-wallet discovery), per-origin approvals, chain switching (wallet_switchEthereumChain/wallet_addEthereumChain), andeth_signTypedData_v4. dApps see "MagicMoney Wallet" alongside other wallets. -
Solana — Wallet Standard + legacy
window.solana:connect,signMessage(e.g. OpenSea SIWS),signTransaction, andsignAndSendTransaction. -
Cardano (CIP-30) — exposed canonically at
window.cardano.magicmoneyand, with VESPR's permission, at thewindow.cardano.vesprcompatibility key for dApps that whitelist it. Both keys reference the same MagicMoney-branded provider and use the same approval-gated signer. MagicMoney is registered in the weld registry asmagicmoney. Implemented methods:getNetworkId,getBalance,getUtxos,getCollateral,getUsedAddresses,getUnusedAddresses,getChangeAddress,getRewardAddresses,signTx,signData,submitTx. -
Midnight (DApp Connector) — registers on
window.midnightfor both API generations: under a stable UUID withrdnsinfo.chainlens.magicmoneyfor 4.x dApps that enumerate the registry, and under the readablemagicmoneykey for legacy (≤3.x) dApps. With Lace's permission, a third entry is registered atwindow.midnight.mnLacecarrying Lace'sio.lace.walletrdns — live Midnight dApps enumerate the registry but then filter it against a hardcoded rdns allowlist, so a wallet outside that list cannot be selected however correctly it registers. All three entries are the same MagicMoney-branded shim over the same approval-gated IPC routes, and the alias is skipped entirely when a genuine Lace is present. Implemented methods:enable/connect,state,serviceUriConfig/getConfiguration,getUnshieldedAddress,getShieldedAddresses,getDustAddress,getUnshieldedBalances,getDustBalance,getConnectionStatus,signData,makeTransfer,submitTransaction.signDataproduces a BIP-340 Schnorr signature from the unshielded (NIGHT) key — the same key behind the wallet'smn_addr…— and returns the spec's{ data, signature, verifyingKey }. Two signing modes, split on whether the payload is a readable message: -
Readable text signs the raw bytes, and
dataechoes the message. This is what the live ecosystem verifies against — Pulse Finance's backend rejects a prefixed signature outright — and it matches Lace, whose rdns this wallet answers to. -
Binary payloads sign behind the connector spec's
midnight_signed_message:<payload byte length>:separator, withdatacarrying the exact prefixed bytes as hex.
That split is a security boundary, not a formatting one. Signing arbitrary bytes unprefixed with the transfer-authorising key would make signData an oracle a dApp could use to get a transaction signed from a "sign this message" prompt; requiring the separator for anything non-textual closes that, since a transaction segment is a serialization of hashes and keys and cannot masquerade as printable UTF-8. Payloads decode strictly from text, hex, or base64 — lenient decoders silently truncate at the first invalid character, which would let a page display one message and have a shorter one signed. Approval-gated per call, and the prompt renders a decoding of the exact bytes being signed rather than a caption supplied alongside them.
- Hardcoded dApp compatibility branding — some dApps hardcode a wallet's label and icon instead of reading the provider metadata they were given. Where MagicMoney owns an authorized compatibility key, the injected provider narrowly replaces those wallet-row/dialog elements with the MagicMoney name and provider icon: the VESPR rows on
app.strikefinance.organdapp.dexhunter.io, and the Lace row onpulsefinance.org. It does not run on other sites, or when a genuine VESPR/Lace provider owns the key. - WalletConnect v2 — pair via URI for dApps that prefer it.
On desktop, dApps run in a built-in pop-out browser with a native network switcher in the toolbar. In the extension, the providers are injected into every page via a document_start content script. On Android, dApps run in native plugin-owned WebViews with document_start provider injection, tab controls, WalletConnect wc: deep links, and approval overlays rendered by the wallet WebView.
The built-in browser can be promoted to your system default browser:
- Windows — Settings registers MagicMoney under
HKCU(ProgId + Default Programs entries, with a dedicatedbrowser-icon.icoshipped outside the asar so the OS chooser shows the proper name and icon) and opens the Windows default-apps page for the final switch. Once set, links from any app cold-start or reuse the running wallet (single-instance lock) and open in the dApp browser — no unlock required to browse; wallet access still goes through the normal per-origin approvals. - Android — a Settings row uses
RoleManagerto request the browser role, and the manifest handleshttp(s)VIEW intents in the native DappBrowser. - The Chrome extension intentionally has no default-browser mode — its "browser" is Chrome itself.
Downloads started in the browser (files, NFT images) are saved natively with a neon progress bar across the top of the window on desktop, and via a native Downloader plugin into the Downloads folder on Android.
On desktop the app minimizes to the system tray on close and keeps running in the background. The tray icon's menu offers Open Wallet, Open Browser (each opens independently — the browser works without the wallet window being visible), and Exit MagicMoney; left-clicking the tray icon reopens the wallet. Closing the browser window still tears down its dApp tabs so background tabs can't drain resources.
The onion button in the desktop and Android browser toolbars enables Tor Mode. On 64-bit Windows, desktop first use downloads the Tor Project Expert Bundle from the official archive, verifies its pinned SHA-256, installs it under Electron's user-data directory, and starts it privately; an already-running service on 127.0.0.1:9050 or 127.0.0.1:9150 is reused. Android starts the bundled Guardian Project Tor Android runtime on a loopback SOCKS endpoint, so users do not need Orbot or another app. A green Android state means both check.torproject.org verified the exit and a v3 onion connection succeeded. Bare .onion addresses default to http:// because Tor already authenticates and encrypts onion traffic. A red Tor blocked panel keeps the proxy configured so browser requests cannot silently fall back to the direct connection, while offering Retry and Turn Off controls. Electron persists the preference and restores the proxy before its first dApp request. Android's WebView proxy API is app-wide, so enabling it applies to every WebView owned by the Android app. The Chrome extension does not expose this switch because its “browser” is the user's ordinary Chrome tabs and a wallet extension cannot safely make them a private per-wallet Tor session.
This is Tor routing, not a claim of Tor Browser anonymity: the embedded Chromium/WebView engines do not reproduce Tor Browser's fingerprint normalization or security patches.
The Swap page is dual-mode, covering both on-chain and cross-chain trades:
- DEX (sign locally) — same-chain swaps aggregate the best price across 0x / 1inch (EVM) and Jupiter (Solana). Cross-chain routes use LI.FI / Rango / SwapKit (THORChain) with a Phantom-style auto-router (independent From/To network selectors). Transactions are signed locally with your own keys.
- Cross-chain exchange (deposit address) — SimpleSwap with a ChangeNOW fallback, used for assets that can't be signed locally as a source (e.g. BTC, ADA, DOT).
Quotes, slippage, gas preflight, and a periodic price-refresh guard are handled in the swap widgets. Cardano on-chain DEX execution is stubbed.
MagicMoney can display your Abstract Global Wallet (a zkSync smart account on Abstract, chainId 2741) inside the same portfolio total. Because an AGW is typically owned by a Privy embedded signer rather than your EOA, auto-discovery isn't possible — you add the AGW address manually in the AgwPanel (Networks tab). Its native ETH, tokens, and NFTs are badged as AGW-sourced and counted in your total. Direct sending is enabled only when your EOA is a verified on-chain owner; otherwise the panel links out to the Abstract Portal.
# Development
npm run dev # Electron + hot reload (regenerates App Hub, builds injects)
npm run build:extension # Chrome extension → dist-extension/
npm run build:capacitor # Android web bundle → dist-capacitor/ + cap sync
npm run android # Build and deploy to a connected device/AVD
# Production builds
npm run build # Electron build → out/
npm run package # Electron installer → dist/
npm run package:publish # Build + publish to GitHub Releases (requires GH_TOKEN)
npm run android:apk # Android release APK (requires android/keystore.properties)
npm run android:aab # Android release AAB for Play Console
# Release (bump version + tag + push → GitHub Actions builds everything)
npm run release:patch # 0.1.1 → 0.1.2
npm run release:minor # 0.1.1 → 0.2.0
npm run release:major # 0.1.1 → 1.0.0Build note: the dApp-injection preloads (
web3-inject,popup-chrome,popup-connect,approval-preload) are bundled by a separatebuild:injectesbuild step intoout/inject/. Thedev,build, andpackagescripts all run it; don't remove it or the in-app dApp browser loses its provider.
Pushing a version tag triggers .github/workflows/release.yml, which:
- Builds the Electron app on Windows, macOS, and Linux in parallel.
- Publishes installers to GitHub Releases via
electron-builder --publish always. - Builds the Android APK/AAB and uploads them to the same release.
- Builds the Chrome extension and uploads the
.zipto the same release.
The release scripts handle the whole flow — bump, commit, tag, push. electron-builder --publish always also uploads the latest.yml / latest-mac.yml / latest-linux.yml update feeds that the in-app Software Update button reads, so publishing a release is all it takes for existing installs to update themselves.
Signing note. Builds are currently unsigned (
CSC_IDENTITY_AUTO_DISCOVERY: false). Windows NSIS and Linux AppImage auto-apply updates fine unsigned; a first-time Windows install may show a SmartScreen warning. macOS is different — Squirrel.Mac refuses to apply an unsigned update, so macOS uses the assisted-download fallback until an Apple Developer ID cert + notarization secrets (CSC_LINK/CSC_KEY_PASSWORD+ notarize creds) are added, at which point macOS can be flipped to silent auto-update with a build-config change.
Android signing note. Update-over-install only works when every APK is signed with the same keystore. CI produces release-signed APK/AAB artifacts when
ANDROID_KEYSTORE_B64,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS, andANDROID_KEY_PASSWORDare set; otherwise it attaches a debug-signed APK for testing.
src/
├── main/ ← Privileged logic (Electron main + shared core)
│ ├── index.ts ← App entry, BrowserWindow, system tray (close-to-tray), startup update check
│ ├── default-browser.ts ← Register as system default browser (Windows HKCU / macOS best-effort)
│ ├── downloads.ts ← Native download handling + progress events for the dApp browser
│ ├── update-manager.ts ← electron-updater state machine (in-app Update button)
│ ├── wallet-core.ts ← BIP-39/32/44 derivation — keys never leave here
│ ├── chain-config.ts ← Default, Privacy Mode, and Testnet Mode network sets
│ ├── api-proxy.ts ← Proxy-first URL/header rewriting to the Worker
│ ├── balance-fetcher.ts ← Native balances across every chain
│ ├── token-fetcher.ts ← ERC-20 / SPL / Cardano assets + NFTs
│ ├── market-fetcher.ts ← CoinGecko prices (cached)
│ ├── native-prices.ts ← Shared native-USD cache (avoids CoinGecko 429s)
│ ├── tx-sender.ts ← Send transactions (EVM / Solana / Cardano / Tron / Doge)
│ ├── tx-history.ts ← Transaction history
│ ├── tx-describe.ts ← Human-readable tx descriptions
│ ├── cardano-pure.ts ← Pure-JS Cardano derivation (CIP-1852 / Icarus)
│ ├── cardano-cip30.ts ← CIP-30 dApp connector implementation
│ ├── cardano-koios.ts ← Keyless Koios fallback for core Cardano ops
│ ├── tron.ts ← TRON HTTP API client (TronGrid fallback)
│ ├── dogecoin.ts ← Dogecoin (BlockCypher + @scure/btc-signer)
│ ├── agw.ts ← Abstract Global Wallet resolution/linking
│ ├── swap-proxy.ts ← DEX/bridge quote routing (LI.FI client-side)
│ ├── swap-executor.ts ← Local signing for DEX swaps
│ ├── simpleswap-client.ts / changenow-client.ts / xchange-client.ts ← Cross-chain exchange
│ ├── browser-manager.ts ← Pop-out dApp browser (WebContentsView)
│ ├── dapp-chain.ts ← Per-session active dApp chain state
│ ├── secure-store.ts ← safeStorage wrapper + config/approved-origins
│ ├── crypto-vault.ts ← AES-256 vault (extension key encryption)
│ ├── supabase-sync.ts ← ChainLens profile sync (signature-gated)
│ ├── wc-client.ts ← WalletConnect v2 (desktop)
│ └── ipc-handlers.ts ← All wallet:* / web3:* / swap:* handlers
├── preload/
│ ├── index.ts ← contextBridge — the only renderer↔main surface
│ ├── web3-inject.ts ← Injected EIP-1193/6963 + Solana + CIP-30 (built to out/inject)
│ ├── popup-chrome.ts ← Branded titlebar for frameless dApp popups
│ ├── popup-connect.ts ← web3-inject + titlebar for auth popups (AGW/Privy login)
│ └── approval-preload.ts ← Approval window preload
├── extension/ ← MV3 adapters (swapped in via Vite aliases)
│ ├── background.ts ← Service worker (replaces ipc-handlers + main)
│ ├── chrome-store.ts ← chrome.storage adapter (replaces secure-store)
│ ├── bridge.ts ← window.wallet shim over chrome.runtime.sendMessage
│ ├── content.ts ← ISOLATED-world relay
│ ├── inject.ts ← MAIN-world EIP-1193/6963 + Solana + CIP-30 + Wallet Standard
│ ├── wc-ext.ts ← WalletConnect v2 (extension)
│ ├── ExtApp.tsx ← Popup wrapper (lock screen, password setup)
│ ├── popup.* / sidepanel.*← Popup and side-panel entries
│ └── manifest.json ← MV3 manifest
├── capacitor/ ← Android/Capacitor adapters
│ ├── CapApp.tsx ← Android app shell, lifecycle, biometric lock screen
│ ├── wallet-local.ts ← Local wallet bridge/router for the WebView runtime
│ ├── capacitor-store.ts ← Preferences-backed encrypted storage
│ ├── dapp-browser.ts ← Typed bridge to the native DappBrowser plugin
│ ├── dapp-inject.ts ← Android dApp provider injection bundle
│ ├── monero-browser.ts ← Browser-compatible Monero backend
│ ├── qr-scan.ts ← ML Kit QR scanner wrapper
│ └── update-check.ts ← GitHub Releases APK update helper
└── renderer/ ← React UI (shared across desktop, extension, and Android)
├── App.tsx / main.tsx ← Router + entry
├── BrowserApp.tsx ← Desktop dApp-browser chrome
├── pages/ ← Dashboard, Market, Swap, AppHub, Profile, Settings, onboarding…
├── components/ ← SendModal, swap widgets, AgwPanel, ChainCard, AddChainModal, DownloadProgressBar, TxList…
└── data/app-hub.ts ← Auto-generated dApp directory (do not edit by hand)
android/
└── app/src/main/java/info/chainlens/magicmoney/
├── MainActivity.java ← Capacitor activity + plugin registration
├── DappBrowserPlugin.java ← Isolated native WebView dApp browser
├── DefaultBrowserPlugin.java ← RoleManager browser-role request (set as default)
├── DownloaderPlugin.java ← Native downloads into the Downloads folder
└── AppInfoPlugin.java ← Installer/source metadata for update checks
The App Hub dApp directory is auto-generated from ChainLens_Files/app-hub-data.js — never edit src/renderer/data/app-hub.ts by hand. The conversion runs automatically via npm predev / prebuild hooks, so npm run dev, build, and package always regenerate it. To regenerate manually:
npm run apphubEdit the source file (ChainLens_Files/app-hub-data.js) to add or remove apps; a JS syntax error there will fail the build.
The Android app is a third build target beside Electron and the MV3 extension: the same
React UI and pure-JS chain core run in a Capacitor WebView, with src/capacitor/ providing
the platform layer (Preferences-backed vault storage, in-process wallet router, biometric
unlock, ML Kit QR scanning, wc: deep links) and a native DappBrowser plugin hosting
dApp pages in separate WebViews with document_start provider injection — untrusted
content never shares a realm with the wallet.
Android builds require JDK 24, Android SDK Platform 37, and Build Tools 37.0.0.
npm run build:capacitor # web bundle + dapp-inject + cap sync
npm run android # build, then deploy to a connected device/AVD
npm run android:apk # signed release APK (needs android/keystore.properties)Sideload install: download magicmoney-android-vX.Y.Z.apk from GitHub Releases, allow
"install unknown apps" for your browser/file manager, and open the APK.
Updates install over the existing app (data intact).
The in-app Software Update row checks GitHub Releases and opens the newer APK's download page. Play Store distribution is future work.
- Chrome Web Store — public extension listing for the first stable release.
- Cardano tx chaining (
supportsTxChaining) — track submitted-but-unconfirmed UTXOs locally so dApps can chain transactions without waiting for confirmation. - Cardano DEX execution — wire the stubbed on-chain swap path (MuesliSwap).
- Fiat on/off-ramp — Transak integration (planned).
- Google Play — the Android APK ships via GitHub Releases today; a Play listing is planned.
See LICENSE.