MagicMoney Wallet — Comprehensive Audit

Claude (Fable 5) · 2026-07-06 · v0.1.1 · Electron + Chrome-extension multi-chain self-custody wallet · Skills used: architecture-review, fullstack-iteration, visual-iteration, playwright-testing, debugging
1. Executive Summary 2. Architecture Assessment 3. UI/UX Assessment 4. Code Quality Assessment 5. Performance Analysis 6. Security Review 7. Database Review 8. State Management Review 9. Dependency Review 10. Playwright Testing Findings 11. Bugs & Issues Found 12. Technical Debt 13. Recommended Refactors 14. Priority Rankings 15. Action Plan

1. Executive Summary

MagicMoney is in very good shape for a pre-1.0 wallet. The security architecture — the part of a wallet you cannot retrofit — is genuinely strong: keys never leave the Electron main process, every window is sandboxed with context isolation, secrets are double-wrapped (PBKDF2-600k → AES-256-GCM → OS keychain) with a KDF upgrade path, provider API keys live only as Cloudflare Worker secrets, the proxy host is pinned to a code-level allowlist, and Supabase writes require a fresh single-use EIP-191 ownership signature. Typecheck is clean, all 37 unit tests and all 3 Playwright e2e tests pass, and a full live walkthrough of the app produced zero JavaScript errors.

The gaps are at the product-hardening layer, not the foundation: no automatic scam-token filtering (phishing airdrops render prominently in the Tokens tab — confirmed on screen), silent dApp chain-switching without a user prompt, thin send-form validation (any string >10 chars is accepted as an address), a duplicated wallet API across the Electron and extension runtimes (~2,900 lines that must be kept in sync by hand), and minimal e2e coverage (3 tests, extension-only). Nothing found is critical; three issues are High.

A−
Security
A−
Architecture
B+
Code Quality
B+
UI / UX
C+
Test Coverage
Verdict: production-viable core with a short, well-defined hardening list. Fix the three High items (scam filtering, chain-switch prompt, send validation) before promoting this beyond early adopters. The strategic debt item is the dual-runtime handler duplication — start chipping at it before the two implementations drift further.

2. Architecture Assessment

Structure

Clean four-layer split with explicit trust boundaries:

LayerRoleNotes
src/main/ (Electron main)All key material, signing, chain RPC, caches, browser windowsThe only place mnemonics exist decrypted; well-enforced
src/preload/Context-isolated IPC bridges (wallet UI, dApp inject, approval, popup)Sandbox + contextIsolation everywhere
src/renderer/React 18 UI, no Node access, strict CSP in packaged buildsReceives only addresses/balances/booleans
src/extension/ + cloudflare-worker/MV3 port of the same wallet; Worker = key-injecting proxy, KV cache, rate limiter, Supabase gatewayWorker is small (1,758 lines), readable, fail-open on cache, fail-closed on prod misconfig

Strengths

Weaknesses

3. UI/UX Assessment

Screenshots below are from a live scripted walkthrough of the built app (isolated profile, public test mnemonic). Overall: a distinctive, consistent dark identity; onboarding is genuinely good (masked seed with explicit reveal, strong warnings, password gate with no-recovery notice).

Welcome
Welcome — clear hierarchy, honest security copy
Seed
Seed phrase masked until explicit reveal ✔
Dashboard
Dashboard — per-chain cards, sparklines, inline send
Tokens
⚠ Tokens tab — scam airdrops render un-flagged; one breaks row layout
Market
Market Watch — clean table, sparklines
Swap
Swap — DEX / Cross-Chain dual mode, MAX button present here
Apps
App Hub — 430 apps, filterable
Settings
Settings — themes, biometrics, connected sites, updates

Findings

4. Code Quality Assessment

5. Performance Analysis

6. Security Review

What's done right (a long list, deliberately)

Gaps

7. Database Review

8. State Management Review

9. Dependency Review

VerdictDetail
GOODOnly 15 runtime deps; crypto is the audited @noble/@scure family; viem 2.37, Electron 43, React 18, Vite 7 all current. overrides pins uuid/ws transitives. No UI framework bloat.
LOWL-6: @supabase/supabase-js is unused. No import anywhere in src/ (sync goes through the Worker). Remove it — dead weight in installs and audit surface.
LOW@solana/web3.js 1.x is legacy (2.x is out) but migration is high-effort/low-reward right now, and it's the main contributor to the 3.7 MB extension worker. Revisit only as part of M-3.
LOWBuild warning: vm-browserify (via node-polyfills) uses eval — confirm it's tree-shaken from the shipped extension or exclude the polyfill.

10. Playwright Testing Findings

11. Bugs & Issues Found

HIGH H-1 · Scam/phishing airdrop tokens render un-flagged

Problem
Tokens with phishing URLs in their names ("$ CLAiM : ethena-v2.com", "halving-btc.net", "www.MaticSlot.io") display as ordinary holdings with huge balances. Confirmed live (screenshot §3).
Why it matters
The #1 real-world attack on wallet users. The wallet's UI lends credibility to the phishing URL; one visit + approval drains the account.
Files
src/main/token-fetcher.ts, src/renderer/pages/DashboardPage.tsx
Solution
Auto-classify at fetch time: URL/domain regex in name or symbol, "claim/airdrop/voucher" keywords, zero-price × enormous-balance heuristic, plus provider spam flags (Alchemy/Moralis both return them — currently unused). Default such tokens into the existing spam bucket with a "possible scam" badge and never render their names as tappable/copyable URLs.
Complexity
Medium (~1 day; heuristics + wire into existing hidden/spam sets)

HIGH H-2 · dApps can switch the active chain silently

Problem
wallet_switchEthereumChain/wallet_addEthereumChain update state and emit chainChanged with no user consent.
Why it matters
Lets a malicious dApp steer context before a transaction; standard wallets prompt on switch. The tx-approval dialog naming the chain is the only backstop.
Files
src/main/ipc-handlers.ts:887-920, mirrored handler in src/extension/background.ts
Solution
Reuse showApprovalWindow ("site wants to switch to Monad") — at minimum for origins not already connected, or once per origin+chain.
Complexity
Low (~2 h, both runtimes)

HIGH H-3 · Send form validation is superficial

Problem
isValidAddress = to.trim().length > 10; no per-chain format/checksum validation, no amount ≤ balance check, no Max button. Errors arrive late as raw provider text.
Why it matters
Irreversible-money UX: a Solana address pasted into a Bitcoin send should fail at the field, not at broadcast. EIP-55 checksum catches real typos.
Files
src/renderer/components/SendModal.tsx:67-68; validators belong in main (wallet-core.ts/per-chain modules) exposed via one wallet:validate-address IPC
Solution
Per-chain validators (viem isAddress w/ checksum, base58+ed25519 decode for Solana, bech32/base58check for BTC/ADA/TRX/DOGE — decoders already in the codebase), inline field feedback, amount cap with fee awareness, Max button (Swap already has one).
Complexity
Medium (~1 day)

MED M-4 · eth_sign returns a personal_sign-scheme signature

Problem
ipc-handlers.ts:972 signs with the EIP-191 prefix, so callers expecting legacy raw-digest eth_sign get signatures that fail verification.
Why it matters
Silent interop failure; and supporting eth_sign at all invites blind-signing phishing.
Files
src/main/ipc-handlers.ts:972-990, extension mirror
Solution
Reject eth_sign with a clear "unsupported for safety" error (MetaMask default since 2024).
Complexity
Trivial

MED M-9 · Sign/send methods skip the connection check

Problem
personal_sign, typed-data, and eth_sendTransaction prompt regardless of whether the origin ever connected.
Why it matters
Prompt-fatigue attacks from arbitrary pages in the dApp browser; EIP-1193 expectation is 4100 (unauthorized) before connection.
Files
src/main/ipc-handlers.ts:947+, extension mirror
Solution
Gate signing methods on getApprovedOrigins().includes(origin); throw code 4100 otherwise.
Complexity
Low

MED M-6 · 189 console 404s per session from dead logo URLs

Problem
Broken token/coin logo URLs re-fetched every refresh; TokenLogo falls back per-mount but nothing remembers failures.
Why it matters
Network waste on a metered proxy, and the noise floor hides real errors during debugging.
Files
src/renderer/pages/DashboardPage.tsx (TokenLogo), src/main/token-fetcher.ts (logo URL sourcing)
Solution
Session-scoped negative cache (Set of failed URLs) consulted before rendering an <img>; optionally validate/drop logo URLs at fetch time.
Complexity
Low

LOW L-1 · Bridge-missing crash in App.tsx effect

Problem
App.tsx:66 calls window.wallet.onBrowserClosed(...) unguarded. Effects run even when the render bailed to the B-1 "bridge unavailable" screen — so the exact failure that screen exists for throws a TypeError first.
Why it matters
The graceful-degradation path self-destructs.
Files
src/renderer/App.tsx:64-68
Solution
window.wallet?.onBrowserClosed (match the optional-chaining of the sibling effects).
Complexity
Trivial

LOW L-2 · Mnemonic whitespace normalization drift

Problem
deriveAddresses collapses inner whitespace; the eight signing helpers only trim+lowercase. Same stored phrase, potentially different seeds.
Why it matters
Latent funds-affecting divergence if any future path stores a non-normalized phrase.
Files
src/main/wallet-core.ts:232-287
Solution
One normalizeMnemonic() used by every entry point; normalize before persisting in saveMnemonic.
Complexity
Trivial

LOW L-3 · Send modal dismissible mid-broadcast

Problem
Escape/overlay-click close during the sending step; the tx still broadcasts but the user never sees the hash.
Files
src/renderer/components/SendModal.tsx:57-65
Solution
Ignore close requests while step === 'sending'.
Complexity
Trivial

LOW L-9 · Long token names break row layout

Problem
Scam token with a long name renders as a vertical character stack overlapping the price column (Tokens screenshot).
Files
src/renderer/pages/DashboardPage.tsx token row
Solution
min-width: 0 on the flex text column + white-space: nowrap; overflow: hidden; text-overflow: ellipsis.
Complexity
Trivial

12. Technical Debt

DebtInterest being paidSeverity
Dual wallet-API implementation (Electron ipc-handlers.ts + extension background.ts, ~2,900 lines)Every handler fix must be written twice; comments already show mirror-patches; drift = divergent security behaviorHIGH
token-fetcher.ts (1,737) / DashboardPage.tsx (1,163) god modulesAny token/NFT change touches a monolith; hard to test in isolationMED
EVM chain metadata split between chain-config.ts and tx-sender.ts mapsAlready caused one shipped regression (BSC quote-but-no-send)MED
Inline styles vs. CSS token systemTheme drift, verbose JSX, inconsistent spacingLOW
Unused @supabase/supabase-js dependencyInstall weight + audit surface for nothingLOW
E2E driver knowledge lives in agent memory, not the repoBus-factor: the working Electron test recipe isn't committedMED

13. Recommended Refactors

  1. Shared wallet-API core (addresses M-1 debt). Extract handler bodies into src/shared/wallet-api.ts: pure functions taking a Store interface (already nearly identical between secure-store and chrome-store) + request context. Electron IPC and the extension message router become thin adapters. Do it incrementally — start with the web3 request switch, which is the highest-drift-risk surface. Complexity: High (spread over releases); payoff: every future feature/security fix lands once.
  2. Split token-fetcher.ts into tokens/evm.ts, tokens/solana.ts, tokens/cardano.ts, tokens/bitcoin.ts, nft-floors.ts, spam-filter.ts (new, hosts H-1 heuristics). Complexity: Medium — mechanical, tests first.
  3. Extract Dashboard data hooks (usePortfolio, useSpamSets, useAutoRefresh) leaving the page as composition. Complexity: Medium.
  4. Unify EVM chain metadata: generate tx-sender's viem chain entries from chain-config.ts (single source of truth incl. explorers/symbols); add a unit test asserting swap-supported ⊆ send-supported. Complexity: Low-Medium.
  5. Extension bundle diet: make the heavy libs consistently dynamic (or accept static and drop the pretense); target < 1.5 MB worker. Complexity: Medium.

Not recommended: a state-management library, a router, a CSS framework, or migrating to @solana/web3.js v2 — all cost with no current payoff.

14. Priority Rankings

PriorityItems
CRITICALNone found.
HIGHH-1 scam-token filtering · H-2 chain-switch prompt · H-3 send validation · (debt) dual-runtime duplication direction set
MEDM-4 reject eth_sign · M-9 connection-gate signing · M-2 god-module splits · M-3 extension bundle · M-5 password strength UX · M-6 logo negative-cache · M-7 Electron e2e suite · M-8 fetcher/chain-parity unit tests
LOWL-1 bridge-missing effect guard · L-2 mnemonic normalization · L-3 modal close mid-send · L-4 hardcoded colors · L-5 inline styles · L-6 remove supabase-js · L-7 aria-labels/focus · L-8 plaintext user keys · L-9 token-row ellipsis

15. Action Plan

PhaseScopeEffort
1 · Quick wins (½ day)L-1, L-2, L-3, L-9, L-6 (rm dep), M-4 (reject eth_sign), M-9 (connection gate) — all trivial/low, several are one-liners; re-run typecheck + tests~4 h
2 · User safety (2-3 days)H-1 spam heuristics + badge; H-2 chain-switch approval (both runtimes); H-3 per-chain address validators + balance cap + Max; M-5 strength meter~3 days
3 · Test hardening (2 days)M-7 commit e2e/electron-smoke.spec.ts from the proven wrapper-main recipe; M-8 unit tests for chain-parity invariants + spam heuristics + fetcher fallbacks; wire e2e into a pre-release script~2 days
4 · Structural (ongoing, next 3-4 releases)Shared wallet-API core (start with web3 switch), token-fetcher split, Dashboard hooks, chain-metadata unification, extension bundle diet, M-6 negative cache, style/token cleanup (L-4/L-5/L-7)1-2 days per release
Validation performed for this audit: npm run typecheck ✔ (both projects) · vitest 37/37 ✔ · npm run test:e2e 3/3 ✔ · npm run build ✔ · npm run build:extension ✔ · live Playwright walkthrough of the built Electron app (isolated profile, 11 screenshots, 0 JS errors, 193 console messages — all resource 404s).