Solana wallet dashboard: connect, send/receive SOL, sign messages, devnet airdrops
Problem
Interacting with Solana requires scattered CLI/RPC tooling; there was no single lightweight UI to connect a wallet, switch clusters, sign messages, and move SOL.
What it does
A single-page React dashboard that wraps the Solana wallet-adapter stack: connect Phantom via WalletStandard, switch between mainnet/testnet/devnet RPC endpoints, view balance, request devnet/testnet airdrops, build and send SystemProgram.transfer transactions, show a QR code + copyable address for receiving, and sign arbitrary messages with client-side ed25519 verification (@noble/curves) and bs58-encoded signature display.
Architecture
[web-ui] React/Vite dashboard (App.tsx, FeaturesGrid, SolSendReceive) (client) [wallet-adapter] @solana/wallet-adapter-react + PhantomWalletAdapter (browser extension wallet) (client) [rpc] Solana JSON-RPC endpoints (mainnet-beta/testnet/devnet public APIs) (external) [crypto] Client-side crypto: @noble/curves ed25519 verify + bs58 (model) web-ui ──▶ wallet-adapter · connect, signMessage, sendTransaction via wallet provider context wallet-adapter ──▶ rpc · signed transaction submission and airdrop requests web-ui ──▶ rpc · getBalance, getSignatureStatus via useConnection web-ui ──▶ crypto · verify signed message against publicKey
Simplified — box-and-arrow sketch of the repo structure, not a system diagram.
Decisions
01
Wallet-adapter context providers instantiated inline in App.tsx
ConnectionProvider/WalletProvider wrap the app with network-driven endpoint selection from a Select control, replacing the separate AppProvider.tsx scaffold (which is hardcoded to devnet and appears unused — inferred from App.tsx duplicating provider setup)
Evidence: src/App.tsx
02
Client-side signature verification of wallet-signed messages
Uses ed25519.verify(signature, msg, publicKey.toBytes()) before showing the bs58 signature, so the toast only displays cryptographically valid signatures
Evidence: src/components/FeaturesGrid.tsx
03
SOL transfers built with SystemProgram.transfer and confirmed via getSignatureStatus
Standard raw transaction construction rather than associated-token or SPL flows, keeping the send path to lamports math and a single status poll
Evidence: src/components/SolSendReceive.tsx
04
RPC endpoints configurable via env with public fallbacks
types.ts maps each Network to an RPC URL with public cluster defaults
Evidence: src/lib/types.ts
05
Airdrop restricted to non-mainnet
handleAirdrop throws when network is mainnet before calling connection.requestAirdrop
Evidence: src/components/FeaturesGrid.tsx
The hard parts
- Transaction confirmation UX: only a single getSignatureStatus poll with no retry, WebSocket subscription, or blockhash handling
- Network switching remounts the wallet adapters (wallets memo depends on network state) and balance only refetches on publicKey/connection change
Known limitations
- README claims real-time balance/transaction history via WebSocket; code only does one-shot getBalance on wallet connect (README.md vs src/components/FeaturesGrid.tsx)
- No real-time transaction history UI exists anywhere in src (inferred from full src listing)
- Env vars use process.env.PUBLIC_RPC_MAINNET and NEXT_PUBLIC_RPC_DEVNET naming — under Vite browser code these resolve to undefined at runtime, so fallback public RPCs are always used (inferred from src/lib/types.ts and vite.config.ts)
- No tests, no CI, no Docker, no license file
- AppProvider.tsx is dead code duplicating provider setup
- Unused heavy dependencies declared (zod, react-hook-form, @hookform/resolvers) — inferred from absence in src components
- Amount input accepts Number(amount) with no validation against zod despite it being a dependency