前端开发:wagmi 与 ethers.js
前端开发:wagmi 与 ethers.js
构建以太坊 DApp 前端需要与区块链进行交互,包括连接钱包、读取链上数据、发送交易等。本文介绍最主流的两个前端库:wagmi/viem(现代 React 生态)和 ethers.js(通用库)。
库的选择
| 库 | 适用场景 | 特点 |
|---|---|---|
| wagmi v2 | React 应用 | Hooks 风格,内置状态管理,推荐 |
| viem | 任意框架 | TypeScript 优先,底层库,高性能 |
| ethers.js v6 | 任意框架 | 成熟稳定,广泛使用 |
| web3.js | 任意框架 | 老牌库,生态广泛 |
ethers.js 基础
安装
npm install ethers连接钱包
import { ethers } from 'ethers';
// 连接 MetaMaskasync function connectWallet() { if (!window.ethereum) { alert('请安装 MetaMask!'); return; }
// 请求用户授权 const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
// 创建 Provider const provider = new ethers.BrowserProvider(window.ethereum);
// 获取 Signer(签名者) const signer = await provider.getSigner();
console.log('连接账户:', accounts[0]); return { provider, signer };}读取链上数据
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
// 查询 ETH 余额const balance = await provider.getBalance('0xAddress');console.log('余额:', ethers.formatEther(balance), 'ETH');
// 查询当前区块号const blockNumber = await provider.getBlockNumber();console.log('当前区块:', blockNumber);
// 获取区块信息const block = await provider.getBlock(blockNumber);console.log('区块时间戳:', block.timestamp);
// 查询交易const tx = await provider.getTransaction('0xTxHash');console.log('交易 Gas:', tx.gasLimit.toString());与合约交互
const ERC20_ABI = [ "function name() view returns (string)", "function symbol() view returns (string)", "function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)", "event Transfer(address indexed from, address indexed to, uint256 value)"];
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');const signer = await provider.getSigner();
// 创建合约实例(只读)const contract = new ethers.Contract( '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC ERC20_ABI, provider);
// 读取合约数据const name = await contract.name();const symbol = await contract.symbol();const balance = await contract.balanceOf('0xAddress');console.log(`${name} (${symbol}): ${ethers.formatUnits(balance, 6)} USDC`);
// 发送交易(需要 signer)const contractWithSigner = contract.connect(signer);const tx = await contractWithSigner.transfer( '0xRecipient', ethers.parseUnits('100', 6) // 100 USDC);await tx.wait();console.log('交易确认:', tx.hash);监听事件
// 监听 Transfer 事件contract.on('Transfer', (from, to, value, event) => { console.log(`转账: ${from} -> ${to}, 金额: ${ethers.formatUnits(value, 6)}`);});
// 查询历史事件const filter = contract.filters.Transfer(null, '0xMyAddress');const events = await contract.queryFilter(filter, -1000); // 最近 1000 个区块events.forEach(event => { console.log('收到转账:', ethers.formatUnits(event.args.value, 6));});签名消息(EIP-712)
// EIP-712 结构化签名const domain = { name: 'My DApp', version: '1', chainId: 1, verifyingContract: '0xContractAddress'};
const types = { Order: [ { name: 'buyer', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'deadline', type: 'uint256' } ]};
const value = { buyer: signer.address, amount: ethers.parseEther('1.0'), deadline: Math.floor(Date.now() / 1000) + 3600};
const signature = await signer.signTypedData(domain, types, value);console.log('签名:', signature);wagmi v2(React 专用)
wagmi 是 React 生态中最流行的以太坊 Hooks 库。
安装
npm install wagmi viem @tanstack/react-query配置
import { http, createConfig } from 'wagmi';import { mainnet, sepolia, arbitrum } from 'wagmi/chains';import { injected, metaMask, walletConnect } from 'wagmi/connectors';
export const config = createConfig({ chains: [mainnet, sepolia, arbitrum], connectors: [ injected(), metaMask(), walletConnect({ projectId: 'YOUR_WALLET_CONNECT_PROJECT_ID' }), ], transports: { [mainnet.id]: http('https://mainnet.infura.io/v3/YOUR_KEY'), [sepolia.id]: http('https://sepolia.infura.io/v3/YOUR_KEY'), [arbitrum.id]: http('https://arb1.arbitrum.io/rpc'), },});// src/app.tsx 或 main.tsximport { WagmiProvider } from 'wagmi';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import { config } from './wagmi';
const queryClient = new QueryClient();
function App({ children }) { return ( <WagmiProvider config={config}> <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> </WagmiProvider> );}连接钱包
import { useConnect, useDisconnect, useAccount } from 'wagmi';
function WalletConnect() { const { address, isConnected, chain } = useAccount(); const { connectors, connect, isPending } = useConnect(); const { disconnect } = useDisconnect();
if (isConnected) { return ( <div> <p>已连接: {address}</p> <p>网络: {chain?.name}</p> <button onClick={() => disconnect()}>断开连接</button> </div> ); }
return ( <div> {connectors.map((connector) => ( <button key={connector.id} onClick={() => connect({ connector })} disabled={isPending} > {connector.name} </button> ))} </div> );}读取余额
import { useBalance, useAccount } from 'wagmi';import { formatEther } from 'viem';
function Balance() { const { address } = useAccount();
const { data: balance, isLoading } = useBalance({ address, });
if (isLoading) return <div>加载中...</div>;
return ( <div> 余额: {balance ? formatEther(balance.value) : '0'} {balance?.symbol} </div> );}读取合约数据
import { useReadContract, useReadContracts } from 'wagmi';import { erc20Abi } from 'viem';
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
function TokenInfo({ userAddress }: { userAddress: string }) { // 读取单个数据 const { data: balance } = useReadContract({ address: USDC_ADDRESS, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress], });
// 批量读取多个数据(减少 RPC 请求) const { data: results } = useReadContracts({ contracts: [ { address: USDC_ADDRESS, abi: erc20Abi, functionName: 'name', }, { address: USDC_ADDRESS, abi: erc20Abi, functionName: 'symbol', }, { address: USDC_ADDRESS, abi: erc20Abi, functionName: 'decimals', }, ], });
const [name, symbol, decimals] = results ?? [];
return ( <div> <p>代币: {name?.result} ({symbol?.result})</p> <p>余额: {balance ? Number(balance) / 10 ** Number(decimals?.result) : 0}</p> </div> );}写入合约(发送交易)
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';import { parseUnits } from 'viem';import { erc20Abi } from 'viem';
function TransferToken() { const { writeContract, data: hash, isPending } = useWriteContract(); const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
function handleTransfer() { writeContract({ address: '0xTokenAddress', abi: erc20Abi, functionName: 'transfer', args: [ '0xRecipient', parseUnits('100', 18), // 100 tokens ], }); }
return ( <div> <button onClick={handleTransfer} disabled={isPending}> {isPending ? '确认中...' : '转账'} </button>
{hash && <p>交易哈希: {hash}</p>} {isConfirming && <p>等待确认...</p>} {isSuccess && <p>交易成功!</p>} </div> );}监听事件
import { useWatchContractEvent } from 'wagmi';
function EventWatcher() { useWatchContractEvent({ address: '0xTokenAddress', abi: erc20Abi, eventName: 'Transfer', onLogs(logs) { logs.forEach(log => { console.log('Transfer:', log.args); }); }, });
return <div>监听转账事件中...</div>;}viem(底层库)
viem 是 wagmi v2 的底层库,也可以单独使用:
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';import { mainnet } from 'viem/chains';
// 公共客户端(只读)const publicClient = createPublicClient({ chain: mainnet, transport: http('https://mainnet.infura.io/v3/YOUR_KEY'),});
// 读取数据const balance = await publicClient.getBalance({ address: '0xAddress' });const blockNumber = await publicClient.getBlockNumber();
// 读取合约const name = await publicClient.readContract({ address: '0xTokenAddress', abi: erc20Abi, functionName: 'name',});
// 钱包客户端(需要私钥或 window.ethereum)import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount('0xPrivateKey');const walletClient = createWalletClient({ account, chain: mainnet, transport: http(),});
// 发送 ETHconst hash = await walletClient.sendTransaction({ to: '0xRecipient', value: parseEther('0.1'),});
// 调用合约写入函数const txHash = await walletClient.writeContract({ address: '0xTokenAddress', abi: erc20Abi, functionName: 'transfer', args: ['0xRecipient', parseEther('100')],});完整 DApp 示例
下面是一个完整的 React DApp 示例,展示代币转账功能:
import { useState } from 'react';import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt} from 'wagmi';import { parseUnits, formatUnits, isAddress } from 'viem';import { erc20Abi } from 'viem';
const TOKEN_ADDRESS = '0xYourTokenAddress';const DECIMALS = 18;
export function TokenTransfer() { const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState('');
const { address } = useAccount();
// 读取余额 const { data: balance, refetch } = useReadContract({ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: 'balanceOf', args: address ? [address] : undefined, query: { enabled: !!address }, });
// 写入合约 const { writeContract, data: hash, isPending, error } = useWriteContract(); const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash, onSuccess: () => refetch(), // 交易成功后刷新余额 });
function handleTransfer(e: React.FormEvent) { e.preventDefault();
if (!isAddress(recipient)) { alert('请输入有效的以太坊地址'); return; }
writeContract({ address: TOKEN_ADDRESS, abi: erc20Abi, functionName: 'transfer', args: [recipient as `0x${string}`, parseUnits(amount, DECIMALS)], }); }
return ( <div> <h2>代币转账</h2> <p>余额: {balance ? formatUnits(balance, DECIMALS) : '0'} TOKEN</p>
<form onSubmit={handleTransfer}> <input type="text" placeholder="接收地址 (0x...)" value={recipient} onChange={e => setRecipient(e.target.value)} /> <input type="number" placeholder="转账数量" value={amount} onChange={e => setAmount(e.target.value)} /> <button type="submit" disabled={isPending || isConfirming}> {isPending ? '等待签名...' : isConfirming ? '确认中...' : '转账'} </button> </form>
{hash && <p>交易哈希: <a href={`https://etherscan.io/tx/${hash}`}>{hash}</a></p>} {isSuccess && <p style={{ color: 'green' }}>转账成功!</p>} {error && <p style={{ color: 'red' }}>错误: {error.message}</p>} </div> );}多链支持
import { useSwitchChain, useChainId } from 'wagmi';import { mainnet, arbitrum, optimism } from 'wagmi/chains';
function ChainSwitcher() { const chainId = useChainId(); const { switchChain, isPending } = useSwitchChain();
const chains = [ { chain: mainnet, name: '以太坊主网' }, { chain: arbitrum, name: 'Arbitrum' }, { chain: optimism, name: 'Optimism' }, ];
return ( <div> <p>当前网络 ID: {chainId}</p> {chains.map(({ chain, name }) => ( <button key={chain.id} onClick={() => switchChain({ chainId: chain.id })} disabled={isPending || chainId === chain.id} > 切换到 {name} </button> ))} </div> );}总结
以太坊前端开发生态选择:
- 新项目推荐:wagmi v2 + viem(React 生态的现代选择)
- 通用项目:ethers.js v6(成熟稳定,不依赖框架)
- 需要考虑:钱包连接、读写合约、事件监听、多链支持
推荐资源:
- wagmi 文档
- viem 文档
- ethers.js 文档
- RainbowKit(wagmi 配套的美观钱包连接 UI)