Non-custodial Ethereum wallet dashboard: connect, switch chains, send ETH, sign messages
Problem
Interacting with Ethereum networks typically requires juggling separate tools for connecting a wallet, switching between mainnet/testnets/L2s, sending funds, and proving address ownership via signatures.
What it does
A React single-page dApp that connects browser wallets via wagmi (injected + Safe connectors) to Ethereum mainnet, Sepolia, and Arbitrum. A landing page offers wallet connection; a dashboard shows the connected account's ETH balance with a USD estimate fetched from CoinGecko, lets users switch chains from a dropdown, send ETH transactions (address validation, wait-for-receipt, toasts), receive funds via a QR code of their address, and sign arbitrary messages — all non-custodial, with no backend of its own.
Architecture
[landing] Home page (wallet connect dialog) (client) [dashboard] Dashboard (balance, send/receive, sign) (client) [wagmi] wagmi/viem client (injected + Safe connectors, 3 chains) (client) [wallet] Browser wallet (MetaMask/Safe) (external) [rpc] Ethereum JSON-RPC (mainnet/Sepolia/Arbitrum) (external) [coingecko] CoinGecko price API (external) landing ──▶ wagmi · connect wallet via dialog dashboard ──▶ wagmi · balance, switchChain, sendTransaction, signMessage wagmi ──▶ wallet · connector handshakes / signing requests wagmi ──▶ rpc · http() transports dashboard ──▶ coingecko · fetch ETH/USD price for balance estimate
Simplified — box-and-arrow sketch of the repo structure, not a system diagram.
Decisions
01
wagmi + viem for all on-chain interaction
Single typed client handles connectors, chain switching, transactions, receipt waiting, and message signing without bespoke RPC code
Evidence: config/config.ts
02
Pure frontend, no backend
All state lives in the wallet and public RPC transports; the only server call is the public CoinGecko price endpoint (inferred from absence of any server code and direct fetch)
Evidence: src/components/AssetOverview.tsx
03
Route guard by connection state instead of protected routes
Dashboard redirects to / in a mount useEffect when the wallet is disconnected, rather than using a route loader/guard
Evidence: src/pages/Dashboard.tsx
04
Connector allowlist by array slicing
Supported wallets are picked by connectors.slice(2), skipping the first two connectors (injected/safe order) — fragile positional filtering
Evidence: src/components/WalletConnector.tsx
05
shadcn/ui vendored components with Tailwind v4
UI primitives (dialog, select, hover-card, tabs) are copied into src/components/ui for fast dark-themed dashboard UI
Evidence: src/components/ui/button.tsx
The hard parts
- Multi-chain state consistency: chain selection, switchChain mutation side effects, and per-chain balance refetch are coordinated manually with useEffect/useMutation callbacks (Dashboard.tsx, SendRecieveTrans.tsx)
- Transaction lifecycle UX: pending/success/error toasts plus balance refetch are wired across two effects reacting to useSendTransaction and useWaitForTransactionReceipt state
- Reconnect-on-mount flow that hides the landing page until isReconnecting settles to avoid connector flash
Known limitations
- No transaction history, token support beyond native ETH, or ENS resolution — the AssetOverview card is hard-labeled 'Ethereum' regardless of selected chain
- Sepolia balances are hard-coded to $0.00 (special-cased in AssetOverview.tsx)
- GSAP is installed and registered but no animation code is actually invoked in the read components (inferred: unused dependency)
- No tests, no CI, no license, no environment configuration; no deployment config (no Dockerfile, no hosting manifest)
- Recipient-address and amount validation is minimal (required-only via react-hook-form; zero/negative amounts not guarded beyond HTML input type)
- Price fetch lacks debounce/caching and can leave USD estimate stale on chain switch