# Dapp Common Skill

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

- Skill: `bitget-wallet-ai-lab/dapp-common-skill` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add bitget-wallet-ai-lab/dapp-common-skill`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitget-wallet-ai-lab/dapp-common-skill/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: bitget-wallet-ai-lab (https://skillmd.com/u/bitget-wallet-ai-lab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bitget-wallet-ai-lab/dapp-common-skill

---


# 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:
1. Go to [dashboard.reown.com](https://dashboard.reown.com/)
2. Sign up / sign in → click **"+ Project"** → select **"AppKit"** → enter project name → **"Create"**
3. Copy the Project ID from the project page
4. 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:**
1. **Never hardcode transaction parameters** — always provide input fields for amounts, recipients, and tokens.
2. **Always show chain context** — display which chain the user is on, and provide a way to switch.
3. **Always format amounts for humans** — show ETH not wei, SOL not lamports, with proper decimal places.
4. **Accept ENS names in address fields** (EVM) — resolve `.eth` names and show the resolved address.
5. **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

```typescript
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

```typescript
function shortenAddress(address: string, chars: number = 4): string {
  if (!address) return '';
  return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}
```

### Chain Configs

```typescript
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);
}
```

### Chain Adapter Interface

For multi-chain DApps, all chain-specific logic goes behind a common interface:

```typescript
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.

```typescript
// 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);
```

### Error Messages

```typescript
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:
1. Chains: EVM (Ethereum). Single-chain.
2. Read `layout.md` + `wallet-connection.md` + `transfer.md` + `transaction-lifecycle.md`
3. Create EVM transfer form with: recipient input, amount input, balance display, gas estimate
4. Show transaction status: signing → pending → confirmed with Etherscan link
5. Handle errors (insufficient funds, user rejection)

### Example 2: Solana Transfer DApp

User says: "Build a DApp to send SOL"

Actions:
1. Chains: Solana. Single-chain.
2. Read `layout.md` + `wallet-connection.md` + `transfer.md` + `transaction-lifecycle.md`
3. Implement Solana adapter: connect via `window.solana.connect()`, transfer via `@solana/web3.js` `SystemProgram.transfer`, sign via `signAndSendTransaction`
4. 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:
1. Chains: EVM + Solana + Bitcoin. Multi-chain (3 families).
2. Read `layout.md` + `wallet-connection.md` + `chain-switching.md`
3. Create chain-family tabs (Ethereum | Solana | Bitcoin), not just an EVM dropdown
4. Implement 3 chain adapters behind `ChainAdapter` interface
5. 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:
1. Chains: EVM. Single-chain.
2. Read `wallet-connection.md` + `signing.md`
3. Implement SIWE flow: connect → sign structured message → verify server-side
4. Provide clear signing preview showing domain, nonce, expiry

### Example 5: Bitcoin + TON DApp

User says: "Build a DApp for Bitcoin and TON transfers"

Actions:
1. Chains: Bitcoin + TON. Multi-chain (2 non-EVM families).
2. Read ALL references
3. Implement Bitcoin adapter (UniSat API: `sendBitcoin(to, sats)`) and TON adapter (`ton_sendTransaction`)
4. Chain-family tabs: Bitcoin | TON. Each with appropriate transfer form, address format, and explorer links
5. 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:
1. **Ask which chains** — do NOT assume EVM only
2. Read ALL references from this skill
3. Read wallet-specific skill (e.g. Bitget Wallet Developer Skill) for provider APIs
4. Implement chain adapters for each requested chain family
5. Build complete DApp with: connection, chain-family switching, transfers, signing, tx tracking
6. Every chain gets proper: address format, amount decimals, explorer links, sign types

## Resources

- [Wagmi Documentation](https://wagmi.sh)
- [Viem Documentation](https://viem.sh)
- [ethers.js Documentation](https://docs.ethers.org)
- [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712)
- [EIP-4361 SIWE](https://eips.ethereum.org/EIPS/eip-4361)
- [Uniswap Interface](https://github.com/Uniswap/interface)

