web3-dapp
npx skills add https://github.com/kaos35/web-3-skill-frontend --skill web3-dapp
Agent 安装分布
Skill 文档
Web3 DApp Development Guide
IMPORTANT: This is a SKILL file, NOT a project. NEVER run npm/bun install in this folder. NEVER create code files here. When creating a new project, ALWAYS ask the user for the project path first or create it in a separate directory (e.g.,
~/Projects/dapp-name).
This guide provides context when working with Web3 DApp projects using Claude Code.
MANDATORY REQUIREMENTS
When creating a new Web3 DApp, you MUST include ALL of the following:
Required Pages (ALWAYS CREATE)
-
app/page.tsx– Landing page with hero section and connect wallet CTA -
app/dashboard/page.tsx– Main dashboard showing wallet overview, balances, NFTs -
app/nfts/page.tsx– NFT gallery with grid/list view and metadata display -
app/swap/page.tsx– Token swap interface (if DeFi features requested) -
app/staking/page.tsx– Staking interface (if yield farming requested) -
app/collection/[address]/page.tsx– Individual NFT collection view -
app/providers.tsx– Client-side providers wrapper (wagmi, RainbowKit, QueryClient)
Required Components (ALWAYS CREATE)
-
components/connect-button.tsx– RainbowKit ConnectButton with custom styling -
components/wallet-info.tsx– Display connected wallet address, balance, chain -
components/nft-card.tsx– NFT display card with image, name, collection -
components/token-balance.tsx– ERC20 token balance display -
components/loading-spinner.tsx– Loading state for async operations -
components/error-boundary.tsx– Error handling for Web3 operations -
components/chain-selector.tsx– Network switching dropdown
Required Hooks (ALWAYS CREATE)
-
hooks/use-contract.ts– Smart contract interaction hook with ABI typing -
hooks/use-nfts.ts– NFT fetching hook with metadata caching -
hooks/use-token-balance.ts– ERC20 balance tracking -
hooks/use-transaction.ts– Transaction submission with status tracking
Required Libraries (ALWAYS INSTALL)
npm install wagmi viem @rainbow-me/rainbowkit
npm install @tanstack/react-query
npm install lucide-react
npm install tailwindcss @radix-ui/react-*
Core libraries:
wagmi– React hooks for Ethereumviem– TypeScript Ethereum library@rainbow-me/rainbowkit– Wallet connection UI@tanstack/react-query– Server state managementlucide-react– Iconstailwindcss– Styling@radix-ui/react-*– Headless UI primitives
RainbowKit Configuration (REQUIRED)
You MUST configure RainbowKit in app/providers.tsx:
'use client';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
mainnet,
polygon,
arbitrum,
base,
bsc,
avalanche,
} from 'wagmi/chains';
const config = getDefaultConfig({
appName: 'Your DApp Name',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // Required for WalletConnect
chains: [mainnet, polygon, arbitrum, base, bsc, avalanche],
ssr: true,
});
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
WalletConnect Project ID (REQUIRED)
You MUST obtain a WalletConnect Project ID from https://cloud.walletconnect.com:
- Create a free account
- Create a new project
- Copy the Project ID
- Store in
.env.local:
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here
Supported Chains Configuration (REQUIRED)
Always include these chains in order:
- Ethereum (mainnet) – Primary chain
- Polygon – Low fees, fast transactions
- Arbitrum – L2 scaling
- Base – Coinbase’s L2
- BSC – Binance Smart Chain
- Avalanche – Fast finality
import { mainnet, polygon, arbitrum, base, bsc, avalanche } from 'wagmi/chains';
const chains = [mainnet, polygon, arbitrum, base, bsc, avalanche] as const;
FORBIDDEN (NEVER USE)
- â
web3.js– Use viem instead - â
ethers.jsv5 – Use viem or ethers v6 - â Direct private key storage in frontend
- â Hardcoded RPC URLs without fallback
- â
window.ethereumdirect access – Always use wagmi - â Storing sensitive data in localStorage
- â Any UI library other than shadcn/ui + Tailwind
- â Server components for Web3 interactions (use ‘use client’)
- â Mutating state directly in render
Wagmi Hook Usage (IMPORTANT)
NEVER call wagmi hooks conditionally or inside callbacks. Always at component top level.
â WRONG:
const handleClick = () => {
const { address } = useAccount(); // ERROR! Hook inside callback
// ...
};
â CORRECT:
function MyComponent() {
const { address, isConnected } = useAccount(); // Top level
const handleClick = () => {
if (address) {
// Use address here
}
};
return <button onClick={handleClick}>Click</button>;
}
POST-CREATION CLEANUP (ALWAYS DO)
After creating a new Next.js project, you MUST:
- Remove default Next.js styling and components:
rm app/globals.css app/page.module.css
rm -rf public/*
-
Update
tailwind.config.tswith custom colors and animations -
Add RainbowKit CSS import in
app/layout.tsx:
import '@rainbow-me/rainbowkit/styles.css';
- Configure
next.config.jsfor static export (optional):
const nextConfig = {
output: 'export',
distDir: 'dist',
}
module.exports = nextConfig
AFTER COMPLETING CODE (ALWAYS RUN)
When you finish writing/modifying code, you MUST run these commands:
npm run build
npm run lint
buildensures no TypeScript or bundling errorslintchecks code quality
Do NOT skip these steps.
Project Creation
When user asks to create a Web3 DApp, you MUST:
-
FIRST ask for the project name and WalletConnect Project ID:
- “What is the project name? (e.g., ‘nft-marketplace’)”
- “Do you have a WalletConnect Project ID? (Get one free at cloud.walletconnect.com)”
-
Create the project using:
npx shadcn@latest init --yes --template next --base-color zinc
- Install required dependencies:
npm install wagmi viem @rainbow-me/rainbowkit @tanstack/react-query lucide-react
- Add shadcn/ui components:
npx shadcn add button card dialog dropdown-menu input select tabs
-
Create
.env.localwith WalletConnect Project ID -
Set up providers, components, and pages
Technology Stack
- Framework: Next.js 14+ with App Router
- Web3: wagmi 2.x, viem 2.x, RainbowKit 2.x
- Styling: Tailwind CSS 3.4+
- UI Components: shadcn/ui + Radix UI
- State Management: TanStack Query (React Query)
- Icons: Lucide React
- Types: TypeScript (strict mode)
WARNING: DO NOT use class components! Always use functional components with hooks.
WARNING: Web3 interactions MUST be in client components (
'use client'directive)
Project Structure
project-root/
âââ app/
â âââ page.tsx # Landing page
â âââ layout.tsx # Root layout with providers
â âââ providers.tsx # Wagmi + RainbowKit providers
â âââ globals.css # Global styles + Tailwind
â âââ dashboard/
â â âââ page.tsx # Dashboard overview
â âââ nfts/
â â âââ page.tsx # NFT gallery
â âââ swap/
â â âââ page.tsx # Token swap
â âââ staking/
â â âââ page.tsx # Staking interface
â âââ collection/
â âââ [address]/
â âââ page.tsx # Collection details
âââ components/
â âââ ui/ # shadcn/ui components
â âââ connect-button.tsx # Wallet connect button
â âââ wallet-info.tsx # Wallet details display
â âââ nft-card.tsx # NFT card component
â âââ nft-grid.tsx # NFT grid layout
â âââ token-balance.tsx # Token balance display
â âââ chain-selector.tsx # Network switcher
â âââ loading-spinner.tsx # Loading states
â âââ error-boundary.tsx # Error handling
âââ hooks/
â âââ use-contract.ts # Contract interactions
â âââ use-nfts.ts # NFT data fetching
â âââ use-token-balance.ts # Token balance hook
â âââ use-transaction.ts # Transaction handling
âââ lib/
â âââ utils.ts # Utility functions (cn helper)
â âââ contracts.ts # Contract ABIs and addresses
â âââ chains.ts # Chain configurations
âââ types/
â âââ nft.ts # NFT TypeScript types
â âââ token.ts # Token types
âââ public/
â âââ images/ # Static assets
âââ .env.local # Environment variables
âââ next.config.js # Next.js config
âââ tailwind.config.ts # Tailwind config
âââ tsconfig.json # TypeScript config
RainbowKit Integration
Basic Setup
// app/providers.tsx
'use client';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { mainnet, polygon, arbitrum, base, bsc, avalanche } from 'wagmi/chains';
const config = getDefaultConfig({
appName: 'Web3 DApp',
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!,
chains: [mainnet, polygon, arbitrum, base, bsc, avalanche],
ssr: true,
});
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
Custom Connect Button
// components/connect-button.tsx
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { Button } from '@/components/ui/button';
export function CustomConnectButton() {
return (
<ConnectButton.Custom>
{({
account,
chain,
openAccountModal,
openChainModal,
openConnectModal,
mounted,
}) => {
const ready = mounted;
const connected = ready && account && chain;
return (
<div
{...(!ready && {
'aria-hidden': true,
style: {
opacity: 0,
pointerEvents: 'none',
userSelect: 'none',
},
})}
>
{(() => {
if (!connected) {
return (
<Button onClick={openConnectModal}>
Connect Wallet
</Button>
);
}
if (chain.unsupported) {
return (
<Button onClick={openChainModal} variant="destructive">
Wrong Network
</Button>
);
}
return (
<div className="flex gap-2">
<Button onClick={openChainModal} variant="outline">
{chain.name}
</Button>
<Button onClick={openAccountModal}>
{account.displayName}
</Button>
</div>
);
})()}
</div>
);
}}
</ConnectButton.Custom>
);
}
Wagmi Hooks Usage
useAccount – Wallet Connection State
import { useAccount, useDisconnect } from 'wagmi';
function WalletStatus() {
const { address, isConnected, chain } = useAccount();
const { disconnect } = useDisconnect();
if (!isConnected) {
return <div>Not connected</div>;
}
return (
<div>
<p>Address: {address}</p>
<p>Chain: {chain?.name}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
useBalance – Native Token Balance
import { useBalance } from 'wagmi';
import { formatEther } from 'viem';
function EthBalance({ address }: { address: `0x${string}` }) {
const { data, isLoading } = useBalance({ address });
if (isLoading) return <div>Loading...</div>;
if (!data) return <div>No balance</div>;
return (
<div>
{formatEther(data.value)} {data.symbol}
</div>
);
}
useReadContract – Read Smart Contract
import { useReadContract } from 'wagmi';
import { erc721Abi } from 'viem';
function TokenOwner({
contractAddress,
tokenId
}: {
contractAddress: `0x${string}`;
tokenId: bigint;
}) {
const { data: owner, isLoading } = useReadContract({
address: contractAddress,
abi: erc721Abi,
functionName: 'ownerOf',
args: [tokenId],
});
if (isLoading) return <div>Loading...</div>;
return <div>Owner: {owner}</div>;
}
useWriteContract – Write to Smart Contract
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';
function TransferButton({
contractAddress,
to,
amount,
}: {
contractAddress: `0x${string}`;
to: `0x${string}`;
amount: string;
}) {
const {
writeContract,
data: hash,
isPending,
error
} = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
const handleTransfer = () => {
writeContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [to, parseEther(amount)],
});
};
return (
<div>
<button
onClick={handleTransfer}
disabled={isPending || isConfirming}
>
{isPending ? 'Confirm in wallet...' :
isConfirming ? 'Confirming...' :
'Transfer'}
</button>
{isSuccess && <div>Transaction confirmed!</div>}
{error && <div>Error: {error.message}</div>}
</div>
);
}
NFT Implementation
NFT Type Definition
// types/nft.ts
export interface NFT {
id: string;
tokenId: string;
contractAddress: `0x${string}`;
name: string;
description?: string;
image: string;
collection?: {
name: string;
address: `0x${string}`;
};
attributes?: Array<{
trait_type: string;
value: string | number;
}>;
}
NFT Card Component
// components/nft-card.tsx
'use client';
import Image from 'next/image';
import { NFT } from '@/types/nft';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
interface NFTCardProps {
nft: NFT;
onClick?: (nft: NFT) => void;
}
export function NFTCard({ nft, onClick }: NFTCardProps) {
return (
<Card
className="overflow-hidden cursor-pointer transition-transform hover:scale-105"
onClick={() => onClick?.(nft)}
>
<div className="aspect-square relative">
<Image
src={nft.image}
alt={nft.name}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<CardHeader className="p-4">
<h3 className="font-semibold truncate">{nft.name}</h3>
{nft.collection && (
<p className="text-sm text-muted-foreground">
{nft.collection.name}
</p>
)}
</CardHeader>
<CardContent className="p-4 pt-0">
<p className="text-xs text-muted-foreground">
Token ID: {nft.tokenId}
</p>
</CardContent>
</Card>
);
}
NFT Grid Component
// components/nft-grid.tsx
'use client';
import { NFT } from '@/types/nft';
import { NFTCard } from './nft-card';
interface NFTGridProps {
nfts: NFT[];
isLoading?: boolean;
onNFTClick?: (nft: NFT) => void;
}
export function NFTGrid({ nfts, isLoading, onNFTClick }: NFTGridProps) {
if (isLoading) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="aspect-square bg-muted animate-pulse rounded-lg"
/>
))}
</div>
);
}
if (nfts.length === 0) {
return (
<div className="text-center py-12">
<p className="text-muted-foreground">No NFTs found</p>
</div>
);
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{nfts.map((nft) => (
<NFTCard
key={`${nft.contractAddress}-${nft.tokenId}`}
nft={nft}
onClick={onNFTClick}
/>
))}
</div>
);
}
useNFTs Hook
// hooks/use-nfts.ts
'use client';
import { useAccount } from 'wagmi';
import { useQuery } from '@tanstack/react-query';
import { NFT } from '@/types/nft';
async function fetchNFTs(address: `0x${string}`, chainId: number): Promise<NFT[]> {
// Use Alchemy, Infura, or OpenSea API
const response = await fetch(
`/api/nfts?address=${address}&chainId=${chainId}`
);
if (!response.ok) {
throw new Error('Failed to fetch NFTs');
}
return response.json();
}
export function useNFTs() {
const { address, chainId } = useAccount();
return useQuery({
queryKey: ['nfts', address, chainId],
queryFn: () => fetchNFTs(address!, chainId!),
enabled: !!address && !!chainId,
staleTime: 60000, // 1 minute
});
}
Token Swap Implementation
Swap Interface
// app/swap/page.tsx
'use client';
import { useState } from 'react';
import { useAccount } from 'wagmi';
import { parseEther, formatEther } from 'viem';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { CustomConnectButton } from '@/components/connect-button';
export default function SwapPage() {
const { isConnected } = useAccount();
const [fromToken, setFromToken] = useState('ETH');
const [toToken, setToToken] = useState('USDC');
const [fromAmount, setFromAmount] = useState('');
if (!isConnected) {
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<h1 className="text-2xl font-bold">Connect Your Wallet</h1>
<p className="text-muted-foreground">
Please connect your wallet to swap tokens
</p>
<CustomConnectButton />
</div>
);
}
return (
<div className="container max-w-md mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>Swap Tokens</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">From</label>
<div className="flex gap-2">
<Input
type="number"
placeholder="0.0"
value={fromAmount}
onChange={(e) => setFromAmount(e.target.value)}
/>
<select
value={fromToken}
onChange={(e) => setFromToken(e.target.value)}
className="border rounded px-2"
>
<option value="ETH">ETH</option>
<option value="USDC">USDC</option>
<option value="USDT">USDT</option>
</select>
</div>
</div>
<div className="flex justify-center">
<Button variant="outline" size="icon">
â
</Button>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">To</label>
<div className="flex gap-2">
<Input type="number" placeholder="0.0" readOnly />
<select
value={toToken}
onChange={(e) => setToToken(e.target.value)}
className="border rounded px-2"
>
<option value="USDC">USDC</option>
<option value="ETH">ETH</option>
<option value="USDT">USDT</option>
</select>
</div>
</div>
<Button className="w-full">Swap</Button>
</CardContent>
</Card>
</div>
);
}
Smart Contract Integration
Contract Configuration
// lib/contracts.ts
import { erc20Abi, erc721Abi } from 'viem';
export const CONTRACTS = {
usdc: {
address: '0xA0b86a33E6441E6C7D3D4B4f6c7e8f9a0B1c2D3e' as `0x${string}`,
abi: erc20Abi,
},
nftCollection: {
address: '0xB1c2D3e4F5a6B7c8D9e0F1a2B3c4D5e6F7a8B9c0' as `0x${string}`,
abi: erc721Abi,
},
} as const;
// Custom contract ABIs
export const stakingAbi = [
{
inputs: [{ name: 'amount', type: 'uint256' }],
name: 'stake',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'unstake',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
] as const;
useContract Hook
// hooks/use-contract.ts
'use client';
import { useReadContract, useWriteContract } from 'wagmi';
import { Abi, Address } from 'viem';
interface UseContractReadProps<TAbi extends Abi> {
address: Address;
abi: TAbi;
functionName: string;
args?: unknown[];
}
export function useContractRead<TAbi extends Abi>({
address,
abi,
functionName,
args,
}: UseContractReadProps<TAbi>) {
return useReadContract({
address,
abi,
functionName,
args,
});
}
interface UseContractWriteProps<TAbi extends Abi> {
address: Address;
abi: TAbi;
functionName: string;
}
export function useContractWrite<TAbi extends Abi>({
address,
abi,
functionName,
}: UseContractWriteProps<TAbi>) {
const { writeContract, ...rest } = useWriteContract();
return {
write: (args: unknown[]) =>
writeContract({
address,
abi,
functionName,
args,
}),
...rest,
};
}
Web3 Auth & Token Gating
NFT Gated Content
// components/gated-content.tsx
'use client';
import { useAccount } from 'wagmi';
import { useReadContract } from 'wagmi';
import { erc721Abi } from 'viem';
import { CustomConnectButton } from './connect-button';
interface GatedContentProps {
nftContract: `0x${string}`;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function GatedContent({
nftContract,
children,
fallback
}: GatedContentProps) {
const { address, isConnected } = useAccount();
const { data: balance, isLoading } = useReadContract({
address: nftContract,
abi: erc721Abi,
functionName: 'balanceOf',
args: address ? [address] : undefined,
query: {
enabled: !!address,
},
});
const hasNFT = balance && balance > 0n;
if (!isConnected) {
return (
<div className="text-center py-8">
<p className="mb-4">Connect your wallet to access this content</p>
<CustomConnectButton />
</div>
);
}
if (isLoading) {
return <div className="text-center py-8">Checking access...</div>;
}
if (!hasNFT) {
return (
fallback || (
<div className="text-center py-8">
<p>You need to own an NFT to access this content</p>
</div>
)
);
}
return <>{children}</>;
}
Staking Implementation
Staking Interface
// app/staking/page.tsx
'use client';
import { useState } from 'react';
import { useAccount } from 'wagmi';
import { parseEther, formatEther } from 'viem';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useContractRead, useContractWrite } from '@/hooks/use-contract';
import { stakingAbi } from '@/lib/contracts';
const STAKING_CONTRACT = '0x...' as `0x${string}`;
export default function StakingPage() {
const { address } = useAccount();
const [stakeAmount, setStakeAmount] = useState('');
const { data: stakedBalance } = useContractRead({
address: STAKING_CONTRACT,
abi: stakingAbi,
functionName: 'balanceOf',
args: address ? [address] : undefined,
});
const { write: stake, isPending: isStaking } = useContractWrite({
address: STAKING_CONTRACT,
abi: stakingAbi,
functionName: 'stake',
});
const { write: unstake, isPending: isUnstaking } = useContractWrite({
address: STAKING_CONTRACT,
abi: stakingAbi,
functionName: 'unstake',
});
const handleStake = () => {
if (!stakeAmount) return;
stake([parseEther(stakeAmount)]);
};
return (
<div className="container max-w-2xl mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>Staking</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">Your Staked Balance</p>
<p className="text-2xl font-bold">
{stakedBalance ? formatEther(stakedBalance) : '0'} ETH
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Stake Amount</label>
<div className="flex gap-2">
<Input
type="number"
placeholder="0.0"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
/>
<Button
onClick={handleStake}
disabled={isStaking || !stakeAmount}
>
{isStaking ? 'Staking...' : 'Stake'}
</Button>
</div>
</div>
<Button
variant="outline"
onClick={() => unstake([])}
disabled={isUnstaking || !stakedBalance}
className="w-full"
>
{isUnstaking ? 'Unstaking...' : 'Unstake All'}
</Button>
</CardContent>
</Card>
</div>
);
}
Error Handling
Web3 Error Boundary
// components/web3-error-boundary.tsx
'use client';
import { Component, ReactNode } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class Web3ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Web3 Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Card className="m-4">
<CardHeader>
<CardTitle>Something went wrong</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground mb-4">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<Button onClick={() => window.location.reload()}>
Reload Page
</Button>
</CardContent>
</Card>
);
}
return this.props.children;
}
}
Environment Variables
Required .env.local
# WalletConnect Project ID (Required)
# Get one free at: https://cloud.walletconnect.com
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here
# Optional: Alchemy/Infura API Keys for enhanced data fetching
NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_key
NEXT_PUBLIC_INFURA_API_KEY=your_infura_key
# Optional: Analytics
NEXT_PUBLIC_ANALYTICS_ID=your_analytics_id
Development Commands
# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Run linter
npm run lint
# Type check
npx tsc --noEmit
Testing Checklist
- Wallet connection works on all supported chains
- Network switching functions correctly
- NFT metadata loads and displays properly
- Token balances update after transactions
- Transaction states (pending, success, error) display correctly
- Mobile responsive design works
- Dark/Light mode toggle functions
- Error boundaries catch and display errors
- Loading states are implemented
- Gas estimation works for write operations
Security Best Practices
- NEVER store private keys in frontend code
- NEVER expose sensitive API keys in client-side code
- Always use
use clientdirective for Web3 components - Validate all user inputs before contract interactions
- Use type-safe ABIs from viem or generated from verified contracts
- Implement proper error handling for all async operations
- Use
parseEther/formatEtherfor ETH amount conversions - Always check
isConnectedbefore requiring wallet interactions - Implement proper loading states to prevent double-submissions
- Use testnets for development (Goerli, Sepolia, Mumbai)
Deployment
Vercel Deployment
- Push code to GitHub
- Import project in Vercel
- Add environment variables in Vercel dashboard
- Deploy!
IPFS Deployment (Optional)
# Build static export
npm run build
# Deploy to IPFS using Pinata or similar
npx pinata-cli upload dist/
After Development
# Final build check
npm run build
# Run all checks
npm run lint && npx tsc --noEmit
# Test on different networks
# - Local (Hardhat/Anvil)
# - Testnet (Sepolia/Goerli)
# - Mainnet (with caution)
NOTE: Always test thoroughly on testnets before mainnet deployment!
Common Pitfalls
- Missing ‘use client’ – Web3 hooks only work in client components
- Wrong chain configuration – Ensure chain IDs match between wagmi and RainbowKit
- ABI mismatches – Use viem’s built-in ABIs or verify contract ABIs
- Gas estimation failures – Handle errors when estimating gas
- CORS issues – Configure API endpoints to allow frontend origin
- Image loading – Use Next.js Image component with proper domains config
- BigInt serialization – Convert BigInt to string before JSON operations