Common DApp development patterns and UI components. Use this skill whenever the user wants to build a DApp, create a wallet-connected web app, add token transfers, chain switching, message signing, transaction status tracking, or any blockchain-interactive frontend. Also use when the user mentions DApp layout, transfer forms, signing panels, gas estimation, chain selectors, wallet connection UI, or multi-chain architecture — even if they don't explicitly say "DApp". Use proactively for any web3 frontend work including React + Vite, Next.js, or Vue projects that interact with wallets. Pair with a wallet-specific skill (e.g. Bitget Wallet Developer Skill) for complete DApp development. Do NOT use for smart contract development, backend services, or wallet-internal implementation.
Standard patterns for building production-quality DApp frontends. Multi-chain by design — supports EVM, Solana, Bitcoin, TON, Aptos, Cosmos, Tron, and Sui.
Before writing any code, clarify the DApp's purpose and scope.
Must ask:
| Question | Why | Do NOT assume |
|---|---|---|
| What does this DApp do? (swap, transfer, NFT mint, dashboard, etc.) | Determines which modules to load | Never assume "just a connect button" |
| Which chains? (EVM, Solana, Bitcoin, TON, Aptos, Cosmos, Tron, Sui) | Determines chain adapter(s) to implement | Never default to EVM-only |
| Single-chain or multi-chain? | If multi-chain, need chain-family tabs | Never assume single-chain |
| Framework preference? (React + Vite, Next.js, Vue) | Determines component syntax | Default to React + Vite if not specified |
| Need token transfers? If so, native only or token too? | Determines transfer form complexity | Never hardcode amount or skip input |
| Need message signing? What types? (login, authorization, typed data) | Determines signing UI | Never skip sign type selector |
Do NOT assume EVM. If the user says "build a DApp" without specifying chains, you MUST ask which chains they want. If the user specifies a wallet (e.g. "Bitget Wallet"), check which chains that wallet supports and ask the user to choose.
If using Wagmi / RainbowKit / WalletConnect (adapter path), also ask:
| Question | Why | Do NOT assume |
|---|---|---|
| Do you have a Reown (WalletConnect) Project ID? | Required for WalletConnect QR / mobile wallet. Not needed for EIP-6963 browser extension only. | Never hardcode 'YOUR_PROJECT_ID' and move on |
If they don't have one, guide them:
.env as VITE_WALLETCONNECT_PROJECT_ID (Vite) or NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID (Next.js)Without a Project ID: EIP-6963 injected wallet discovery (browser extensions) still works. Only WalletConnect QR code and mobile wallet connections are disabled.
| User Intent | Load References |
|---|---|
| "Build a DApp" (generic, EVM) | layout.md + adapter-integration.md + ux-patterns.md + ask what features |
| "Build a DApp" (non-EVM or multi-chain) | layout.md + wallet-connection.md + ux-patterns.md + ask what features |
| "Use Wagmi / RainbowKit / WalletConnect" | adapter-integration.md |
| "Add transfer / send tokens" | transfer.md + transaction-lifecycle.md + gas-and-fees.md |
| "Add ERC-20 token transfer" | transfer.md + token-approval.md + gas-and-fees.md |
| "Add chain switching / multi-chain" | chain-switching.md |
| "Add message signing / SIWE / login" | signing.md |
| "Show transaction status / history" | transaction-lifecycle.md + ux-patterns.md (toast) |
| "Show gas fees / fee estimate" | gas-and-fees.md |
| "Full-featured DApp" | All references |
Always load
ux-patterns.mdfor any DApp — it covers loading states, toasts, empty states, and confirmation dialogs that every DApp needs.
A production DApp should let users choose their connection method (browser extension, WalletConnect QR, mobile deep link, etc.), not hardcode a single method. The "Connect Wallet" button should open a modal showing all available options.
Recommended approach by framework:
React + EVM (any) → adapter-integration.md (RainbowKit — connection modal built-in)
Vue / Angular / Vanilla JS → adapter-integration.md (Web3Modal — framework-agnostic modal)
Non-EVM chains (SOL/BTC/TON) → wallet-connection.md (direct provider, no adapter ecosystem yet)
Multi-chain (EVM + non-EVM) → adapter-integration.md (EVM) + wallet-connection.md (non-EVM)
Default recommendation: Use RainbowKit or Web3Modal. They provide a connection modal that auto-detects installed wallets (EIP-6963), shows WalletConnect QR code for mobile, and lets the user choose. Do NOT build a single-method "Connect Bitget Wallet" button for production EVM DApps — always offer multiple connection paths.
Connection methods the modal should support:
| Method | How It Works | When Available |
|---|---|---|
| Browser Extension (EIP-6963) | Auto-detects installed wallets, user clicks one | Wallet extension installed |
| WalletConnect QR | Scan QR code with mobile wallet | Always (needs Project ID) |
| Deep Link | Opens mobile wallet app directly | Mobile browser |
| Email / Social | Embedded wallet via Reown AppKit | If configured |
When using adapters, Wagmi hooks replace the custom hooks from wallet-connection.md. But the UI components (TransferForm, SigningPanel, TxStatus, etc.) remain the same — they are adapter-agnostic.
Every DApp MUST follow this structure. Non-negotiable.
Adapter-based DApp (Wagmi/RainbowKit — recommended for React + EVM):
src/
├── config/
│ └── wagmi.ts # Wagmi config + chains + connectors + wallet list
├── components/
│ ├── TransferForm.tsx # Same as below — adapter-agnostic
│ ├── SigningPanel.tsx # Same as below — adapter-agnostic
│ ├── TxStatus.tsx # Transaction lifecycle display
│ └── Layout.tsx # Use <ConnectButton /> from RainbowKit
├── utils/
│ ├── format.ts # Address shortening, amount formatting
│ ├── chains.ts # Chain configs
│ └── errors.ts # User-friendly error messages
├── App.tsx # WagmiProvider + RainbowKitProvider
└── main.tsx
Single-chain DApp (direct provider):
src/
├── components/
│ ├── WalletStatus.tsx # Header: address + chain + balance
│ ├── TransferForm.tsx # Amount + recipient + token + send
│ ├── SigningPanel.tsx # Message input + type selector + sign
│ ├── TxStatus.tsx # Transaction lifecycle display
│ └── Layout.tsx # Page shell with header + content
├── hooks/
│ ├── useWalletConnection.ts # Connect / disconnect / account state
│ ├── useTransfer.ts # Build + send + track transaction
│ ├── useSigning.ts # Sign messages + verify
│ └── useBalance.ts # Native + token balance queries
├── utils/
│ ├── format.ts # Address shortening, amount formatting
│ ├── chains.ts # Chain configs (name, RPC, explorer, icon)
│ └── errors.ts # User-friendly error messages
├── App.tsx # Router + providers
└── main.tsx # Entry point
Multi-chain DApp (2+ chain families):
src/
├── chains/ # ← Chain adapter layer
│ ├── types.ts # ChainAdapter interface
│ ├── evm.ts # EVM adapter (ethers.js)
│ ├── solana.ts # Solana adapter (@solana/web3.js)
│ ├── bitcoin.ts # Bitcoin adapter (UniSat API)
│ ├── ton.ts # TON adapter (TonConnect)
│ └── index.ts # Registry: chain → adapter mapping
├── components/
│ ├── WalletStatus.tsx # Header: address + chain + balance
│ ├── ChainFamilySelector.tsx # EVM / Solana / Bitcoin / TON tabs
│ ├── ChainSelector.tsx # Sub-chain selector (within EVM)
│ ├── TransferForm.tsx # Uses active chain adapter
│ ├── SigningPanel.tsx # Adapts sign types per chain
│ ├── TxStatus.tsx # Transaction lifecycle display
│ └── Layout.tsx # Page shell with header + content
├── hooks/
│ ├── useWalletConnection.ts # Multi-chain connect / disconnect
│ ├── useActiveChain.ts # Current chain family + sub-chain
│ ├── useTransfer.ts # Delegates to chain adapter
│ ├── useSigning.ts # Delegates to chain adapter
│ └── useBalance.ts # Delegates to chain adapter
├── utils/
│ ├── format.ts # Address shortening, amount formatting
│ ├── chains.ts # All chain configs (EVM + non-EVM)
│ └── errors.ts # User-friendly error messages
├── App.tsx
└── main.tsx
These rules apply to ALL DApps regardless of wallet or chain. Non-negotiable.
Inputs & Data:
.eth names and show the resolved address.Transaction Lifecycle: 6. Always show transaction status — use a state machine: idle → signing → pending → confirmed/failed. 7. Always link to explorer — after any on-chain action, show a clickable link to the block explorer. 8. Show gas fee estimate before confirmation — never surprise users with costs. Show Slow/Standard/Fast tiers for EVM. 9. Show pre-wallet confirmation dialog — preview the action in the DApp BEFORE triggering the wallet popup. 10. Use toast notifications — persistent toasts for tx lifecycle so users see status even after navigating away.
Token Approvals (ERC-20): 11. Show approval step explicitly — "Step 1: Approve" → "Step 2: Transfer" with clear labeling. 12. Default to exact-amount approvals — never infinite unless user explicitly opts in. 13. Check existing allowance — don't force unnecessary approvals.
Error Handling & Security: 14. Always handle errors with user-friendly messages — map error codes to plain language. 15. Show what users are signing — display message content before asking for signature; never sign opaque data. 16. Separate signing from broadcasting — provide options for sign-only operations.
Navigation & State: 17. Always provide cancel/back paths — users should never be stuck. 18. Always persist wallet connection — reconnect on page reload. 19. Support both mainnet and testnet — with a clear visual indicator of which environment.
Visual Polish: 20. Use skeleton loading — show shimmering placeholders while data loads, never blank screens. 21. Show empty states with CTAs — "No transactions yet" with actionable guidance, not blank space. 22. Provide copy buttons — on addresses, signatures, and transaction hashes. 23. Support dark mode — respect system preference via CSS custom properties.
Load the relevant reference files and follow the patterns. Every reference includes:
function formatAmount(raw: string | bigint, decimals: number = 18): string {
const value = typeof raw === 'string' ? BigInt(raw) : raw;
const divisor = BigInt(10 ** decimals);
const whole = value / divisor;
const fraction = value % divisor;
const fractionStr = fraction.toString().padStart(decimals, '0').slice(0, 6);
return `${whole}.${fractionStr}`.replace(/\.?0+$/, '') || '0';
}
function parseAmount(display: string, decimals: number = 18): bigint {
const [whole = '0', frac = ''] = display.split('.');
const padded = frac.padEnd(decimals, '0').slice(0, decimals);
return BigInt(whole + padded);
}
function shortenAddress(address: string, chars: number = 4): string {
if (!address) return '';
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}
type ChainFamily = 'evm' | 'solana' | 'bitcoin' | 'ton' | 'aptos' | 'cosmos' | 'tron' | 'sui';
interface ChainConfig {
id: string; // unique key: "ethereum", "solana-mainnet", "bitcoin"
family: ChainFamily;
chainId?: number; // EVM only
name: string;
symbol: string;
decimals: number;
rpcUrl: string;
explorerUrl: string;
explorerTxPath: string; // "/tx/" for EVM, "/transaction/" for Solana, etc.
explorerName: string;
isTestnet: boolean;
}
const CHAINS: Record<string, ChainConfig> = {
// EVM
'ethereum': { id: 'ethereum', family: 'evm', chainId: 1, name: 'Ethereum', symbol: 'ETH', decimals: 18, rpcUrl: 'https://eth.llamarpc.com', explorerUrl: 'https://etherscan.io', explorerTxPath: '/tx/', explorerName: 'Etherscan', isTestnet: false },
'bsc': { id: 'bsc', family: 'evm', chainId: 56, name: 'BNB Chain', symbol: 'BNB', decimals: 18, rpcUrl: 'https://bsc-dataseed1.binance.org', explorerUrl: 'https://bscscan.com', explorerTxPath: '/tx/', explorerName: 'BscScan', isTestnet: false },
'polygon': { id: 'polygon', family: 'evm', chainId: 137, name: 'Polygon', symbol: 'POL', decimals: 18, rpcUrl: 'https://polygon-rpc.com', explorerUrl: 'https://polygonscan.com', explorerTxPath: '/tx/', explorerName: 'PolygonScan', isTestnet: false },
'arbitrum': { id: 'arbitrum', family: 'evm', chainId: 42161, name: 'Arbitrum', symbol: 'ETH', decimals: 18, rpcUrl: 'https://arb1.arbitrum.io/rpc', explorerUrl: 'https://arbiscan.io', explorerTxPath: '/tx/', explorerName: 'Arbiscan', isTestnet: false },
'base': { id: 'base', family: 'evm', chainId: 8453, name: 'Base', symbol: 'ETH', decimals: 18, rpcUrl: 'https://mainnet.base.org', explorerUrl: 'https://basescan.org', explorerTxPath: '/tx/', explorerName: 'BaseScan', isTestnet: false },
'sepolia': { id: 'sepolia', family: 'evm', chainId: 11155111, name: 'Sepolia', symbol: 'ETH', decimals: 18, rpcUrl: 'https://rpc.sepolia.org', explorerUrl: 'https://sepolia.etherscan.io', explorerTxPath: '/tx/', explorerName: 'Etherscan (Sepolia)', isTestnet: true },
// Solana
'solana': { id: 'solana', family: 'solana', name: 'Solana', symbol: 'SOL', decimals: 9, rpcUrl: 'https://api.mainnet-beta.solana.com', explorerUrl: 'https://solscan.io', explorerTxPath: '/tx/', explorerName: 'Solscan', isTestnet: false },
'solana-devnet': { id: 'solana-devnet', family: 'solana', name: 'Solana Devnet', symbol: 'SOL', decimals: 9, rpcUrl: 'https://api.devnet.solana.com', explorerUrl: 'https://explorer.solana.com', explorerTxPath: '/tx/', explorerName: 'Solana Explorer (Devnet)', isTestnet: true },
// Bitcoin
'bitcoin': { id: 'bitcoin', family: 'bitcoin', name: 'Bitcoin', symbol: 'BTC', decimals: 8, rpcUrl: '', explorerUrl: 'https://mempool.space', explorerTxPath: '/tx/', explorerName: 'Mempool', isTestnet: false },
// TON
'ton': { id: 'ton', family: 'ton', name: 'TON', symbol: 'TON', decimals: 9, rpcUrl: 'https://toncenter.com/api/v2', explorerUrl: 'https://tonviewer.com', explorerTxPath: '/transaction/', explorerName: 'TonViewer', isTestnet: false },
// Aptos
'aptos': { id: 'aptos', family: 'aptos', name: 'Aptos', symbol: 'APT', decimals: 8, rpcUrl: 'https://fullnode.mainnet.aptoslabs.com/v1', explorerUrl: 'https://explorer.aptoslabs.com', explorerTxPath: '/txn/', explorerName: 'Aptos Explorer', isTestnet: false },
// Tron
'tron': { id: 'tron', family: 'tron', name: 'Tron', symbol: 'TRX', decimals: 6, rpcUrl: 'https://api.trongrid.io', explorerUrl: 'https://tronscan.org', explorerTxPath: '/#/transaction/', explorerName: 'TronScan', isTestnet: false },
// Sui
'sui': { id: 'sui', family: 'sui', name: 'Sui', symbol: 'SUI', decimals: 9, rpcUrl: 'https://fullnode.mainnet.sui.io', explorerUrl: 'https://suiscan.xyz', explorerTxPath: '/tx/', explorerName: 'SuiScan', isTestnet: false },
// Cosmos
'cosmos': { id: 'cosmos', family: 'cosmos', name: 'Cosmos Hub', symbol: 'ATOM', decimals: 6, rpcUrl: 'https://rpc.cosmos.network', explorerUrl: 'https://www.mintscan.io/cosmos', explorerTxPath: '/tx/', explorerName: 'Mintscan', isTestnet: false },
};
function getExplorerTxUrl(chainId: string, txHash: string): string {
const chain = CHAINS[chainId];
return chain ? `${chain.explorerUrl}${chain.explorerTxPath}${txHash}` : `https://etherscan.io/tx/${txHash}`;
}
function getChainsByFamily(family: ChainFamily): ChainConfig[] {
return Object.values(CHAINS).filter(c => c.family === family);
}
For multi-chain DApps, all chain-specific logic goes behind a common interface:
interface ChainAdapter {
family: ChainFamily;
connect(): Promise<string>; // → address
disconnect(): void;
getBalance(address: string): Promise<string>; // → human-readable
transfer(to: string, amount: string): Promise<string>; // → tx hash or signature
signMessage(message: string): Promise<string>; // → signature
getExplorerTxUrl(txHash: string): string;
formatAddress(address: string): string; // → shortened display
getSupportedSignTypes(): string[]; // e.g. ['personal_sign', 'eth_signTypedData_v4']
}
Each chain implements this interface differently (see reference files for per-chain implementations). The UI components use this interface — they never call chain-specific APIs directly.
// Usage in components — chain-agnostic
const adapter = useActiveChainAdapter(); // returns the adapter for current chain
const balance = await adapter.getBalance(address);
const txHash = await adapter.transfer(to, amount);
function getUserFriendlyError(error: any): string {
const code = error?.code;
const msg = (error?.message || '').toLowerCase();
if (code === 4001 || /reject|cancel|denied|user/i.test(msg))
return 'You rejected the request. Try again when ready.';
if (code === 4100)
return 'Not authorized. Please connect your wallet first.';
if (code === 4902)
return 'This network is not available in your wallet. Please add it manually.';
if (code === -32002)
return 'A wallet request is already pending. Please check your wallet.';
if (code === -32603 || msg.includes('internal'))
return 'Something went wrong. Please try again.';
if (msg.includes('insufficient funds'))
return 'Insufficient balance for this transaction.';
if (msg.includes('nonce'))
return 'Transaction conflict. Please reset your wallet nonce or wait.';
return error?.message || 'An unexpected error occurred.';
}
User says: "Build a DApp where users can send ETH to an address"
Actions:
layout.md + wallet-connection.md + transfer.md + transaction-lifecycle.mdUser says: "Build a DApp to send SOL"
Actions:
layout.md + wallet-connection.md + transfer.md + transaction-lifecycle.mdwindow.solana.connect(), transfer via @solana/web3.js SystemProgram.transfer, sign via signAndSendTransactionUser says: "Build a dashboard that shows balances across Ethereum, Solana, and Bitcoin"
Actions:
layout.md + wallet-connection.md + chain-switching.mdChainAdapter interfaceUser says: "Add wallet-based login to my app"
Actions:
wallet-connection.md + signing.mdUser says: "Build a DApp for Bitcoin and TON transfers"
Actions:
sendBitcoin(to, sats)) and TON adapter (ton_sendTransaction)sats for the API), TON amounts in TON (9 decimals, nanotons for the API)User says: "Build a DApp with Bitget Wallet"
Actions: