跳转到内容

前端开发:wagmi 与 ethers.js

前端开发:wagmi 与 ethers.js

构建以太坊 DApp 前端需要与区块链进行交互,包括连接钱包、读取链上数据、发送交易等。本文介绍最主流的两个前端库:wagmi/viem(现代 React 生态)和 ethers.js(通用库)。

库的选择

适用场景特点
wagmi v2React 应用Hooks 风格,内置状态管理,推荐
viem任意框架TypeScript 优先,底层库,高性能
ethers.js v6任意框架成熟稳定,广泛使用
web3.js任意框架老牌库,生态广泛

ethers.js 基础

安装

Terminal window
npm install ethers

连接钱包

import { ethers } from 'ethers';
// 连接 MetaMask
async 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 库。

安装

Terminal window
npm install wagmi viem @tanstack/react-query

配置

src/wagmi.ts
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.tsx
import { 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(),
});
// 发送 ETH
const 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 示例,展示代币转账功能:

components/TokenTransfer.tsx
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(成熟稳定,不依赖框架)
  • 需要考虑:钱包连接、读写合约、事件监听、多链支持

推荐资源: