| name | dapp-common-skill |
| description | 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.
|
| license | MIT |
| metadata | {"author":"Bitget Wallet","version":"1.0.0","tags":["dapp","frontend","react","web3","ux","transfer","signing","chain-switching"]} |
DApp Common Skill
Standard patterns for building production-quality DApp frontends. Multi-chain by design — supports EVM, Solana, Bitcoin, TON, Aptos, Cosmos, Tron, and Sui.
Instructions
Step 1: Understand What the User Wants
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:
- Go to dashboard.reown.com
- Sign up / sign in → click "+ Project" → select "AppKit" → enter project name → "Create"
- Copy the Project ID from the project page
- Store it in
.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.
Step 2: Route to the Right References
| 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.md for any DApp — it covers loading states, toasts, empty states, and confirmation dialogs that every DApp needs.
Connection Architecture Decision
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.
Step 3: Apply Standard DApp Architecture
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
Step 4: Follow DApp UX Rules
These rules apply to ALL DApps regardless of wallet or chain. Non-negotiable.
Inputs & Data:
- Never hardcode transaction parameters — always provide input fields for amounts, recipients, and tokens.
- Always show chain context — display which chain the user is on, and provide a way to switch.
- Always format amounts for humans — show ETH not wei, SOL not lamports, with proper decimal places.
- Accept ENS names in address fields (EVM) — resolve
.eth names and show the resolved address.
- Validate addresses per chain — use chain-specific regex, don't accept invalid formats.
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.
Step 5: Implement Features
Load the relevant reference files and follow the patterns. Every reference includes:
- TypeScript types
- React hook implementation
- Component template
- Error handling
- Usage example
Core Knowledge (No Reference Loading Required)
Amount Formatting
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);
}
Address Display
function shortenAddress(address: string, chars: number = 4): string {
if (!address) return '';
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}
Chain Configs
type ChainFamily = 'evm' | 'solana' | 'bitcoin' | 'ton' | 'aptos' | 'cosmos' | 'tron' | 'sui';
interface ChainConfig {
id: string;
family: ChainFamily;
chainId?: number;
name: string;
symbol: string;
decimals: number;
rpcUrl: string;
explorerUrl: string;
explorerTxPath: string;
explorerName: string;
isTestnet: boolean;
}
const CHAINS: Record<string, ChainConfig> = {
'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': { 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': { id: 'bitcoin', family: 'bitcoin', name: 'Bitcoin', symbol: 'BTC', decimals: 8, rpcUrl: '', explorerUrl: 'https://mempool.space', explorerTxPath: '/tx/', explorerName: 'Mempool', isTestnet: false },
'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': { 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': { 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': { 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': { 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);
}
Chain Adapter Interface
For multi-chain DApps, all chain-specific logic goes behind a common interface:
interface ChainAdapter {
family: ChainFamily;
connect(): Promise<string>;
disconnect(): void;
getBalance(address: string): Promise<string>;
transfer(to: string, amount: string): Promise<string>;
signMessage(message: string): Promise<string>;
getExplorerTxUrl(txHash: string): string;
formatAddress(address: string): string;
getSupportedSignTypes(): string[];
}
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.
const adapter = useActiveChainAdapter();
const balance = await adapter.getBalance(address);
const txHash = await adapter.transfer(to, amount);
Error Messages
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.';
}
Examples
Example 1: EVM Transfer DApp
User says: "Build a DApp where users can send ETH to an address"
Actions:
- Chains: EVM (Ethereum). Single-chain.
- Read
layout.md + wallet-connection.md + transfer.md + transaction-lifecycle.md
- Create EVM transfer form with: recipient input, amount input, balance display, gas estimate
- Show transaction status: signing → pending → confirmed with Etherscan link
- Handle errors (insufficient funds, user rejection)
Example 2: Solana Transfer DApp
User says: "Build a DApp to send SOL"
Actions:
- Chains: Solana. Single-chain.
- Read
layout.md + wallet-connection.md + transfer.md + transaction-lifecycle.md
- Implement Solana adapter: connect via
window.solana.connect(), transfer via @solana/web3.js SystemProgram.transfer, sign via signAndSendTransaction
- Show amounts in SOL (9 decimals), link to Solscan
Example 3: Multi-Chain Dashboard (EVM + Solana + Bitcoin)
User says: "Build a dashboard that shows balances across Ethereum, Solana, and Bitcoin"
Actions:
- Chains: EVM + Solana + Bitcoin. Multi-chain (3 families).
- Read
layout.md + wallet-connection.md + chain-switching.md
- Create chain-family tabs (Ethereum | Solana | Bitcoin), not just an EVM dropdown
- Implement 3 chain adapters behind
ChainAdapter interface
- Display balance per chain with chain-appropriate formatting and explorers
Example 4: Sign-In with Ethereum (SIWE)
User says: "Add wallet-based login to my app"
Actions:
- Chains: EVM. Single-chain.
- Read
wallet-connection.md + signing.md
- Implement SIWE flow: connect → sign structured message → verify server-side
- Provide clear signing preview showing domain, nonce, expiry
Example 5: Bitcoin + TON DApp
User says: "Build a DApp for Bitcoin and TON transfers"
Actions:
- Chains: Bitcoin + TON. Multi-chain (2 non-EVM families).
- Read ALL references
- Implement Bitcoin adapter (UniSat API:
sendBitcoin(to, sats)) and TON adapter (ton_sendTransaction)
- Chain-family tabs: Bitcoin | TON. Each with appropriate transfer form, address format, and explorer links
- Bitcoin amounts in BTC (8 decimals,
sats for the API), TON amounts in TON (9 decimals, nanotons for the API)
Example 6: Full-Featured Multi-Chain DApp
User says: "Build a DApp with Bitget Wallet"
Actions:
- Ask which chains — do NOT assume EVM only
- Read ALL references from this skill
- Read wallet-specific skill (e.g. Bitget Wallet Developer Skill) for provider APIs
- Implement chain adapters for each requested chain family
- Build complete DApp with: connection, chain-family switching, transfers, signing, tx tracking
- Every chain gets proper: address format, amount decimals, explorer links, sign types
Resources