noise -> signal

000

press any key to skip

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

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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

Command palette

Jump to a section, project or link