name: wallet-integration description: 使用 wagmi 和 viem 为 dApp 提供钱包连接和交易管理。支持多种连接器、链切换、EIP-712 签名和硬件钱包集成。 allowed-tools: Read, Grep, Write, Bash, Edit, Glob, WebFetch
钱包集成技能
使用 wagmi 和 viem 为 Web3 dApp 提供专业的钱包连接和交易管理。
能力
- 连接器配置:使用多个连接器设置 wagmi
- 连接流程:实现钱包连接用户体验
- 链管理:处理链切换和网络错误
- 交易:通过 gas 估算执行交易
- 错误处理:解析并显示交易错误
- EIP-712 签名:实现类型化数据签名
- 事件处理:响应钱包事件
- 硬件钱包:支持 Ledger、Trezor、WalletConnect
安装
# 安装 wagmi 和 viem
npm install wagmi viem @tanstack/react-query
# 可选 UI 套件
npm install @rainbow-me/rainbowkit # 或
npm install @web3modal/wagmi
配置
基础 wagmi 配置
// config/wagmi.ts
import { createConfig, http } from "wagmi";
import { mainnet, sepolia, polygon, arbitrum } from "wagmi/chains";
import { injected, walletConnect, coinbaseWallet } from "wagmi/connectors";
export const config = createConfig({
chains: [mainnet, sepolia, polygon, arbitrum],
connectors: [
injected(),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
}),
coinbaseWallet({
appName: "My dApp",
}),
],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_MAINNET_RPC),
[sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC),
[polygon.id]: http(process.env.NEXT_PUBLIC_POLYGON_RPC),
[arbitrum.id]: http(process.env.NEXT_PUBLIC_ARBITRUM_RPC),
},
});
RainbowKit 设置
// config/rainbowkit.ts
import "@rainbow-me/rainbowkit/styles.css";
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { mainnet, sepolia, polygon } from "wagmi/chains";
export const config = getDefaultConfig({
appName: "My dApp",
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
chains: [mainnet, sepolia, polygon],
ssr: true,
});
提供者设置
// app/providers.tsx
"use client";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "./config/wagmi";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
连接组件
连接按钮
// components/ConnectButton.tsx
import { useAccount, useConnect, useDisconnect } from "wagmi";
export function ConnectButton() {
const { address, isConnected } = useAccount();
const { connect, connectors, isPending, error } = useConnect();
const { disconnect } = useDisconnect();
if (isConnected) {
return (
<div>
<p>
{address?.slice(0, 6)}...{address?.slice(-4)}
</p>
<button onClick={() => disconnect()}>断开连接</button>
</div>
);
}
return (
<div>
{connectors.map((connector) => (
<button
key={connector.id}
onClick={() => connect({ connector })}
disabled={isPending}
>
{isPending ? "连接中..." : `连接 ${connector.name}`}
</button>
))}
{error && <p>错误: {error.message}</p>}
</div>
);
}
账户显示
// components/Account.tsx
import { useAccount, useBalance, useEnsName, useEnsAvatar } from "wagmi";
export function Account() {
const { address, chain } = useAccount();
const { data: balance } = useBalance({ address });
const { data: ensName } = useEnsName({ address });
const { data: ensAvatar } = useEnsAvatar({ name: ensName ?? undefined });
return (
<div>
{ensAvatar && <img src={ensAvatar} alt="ENS 头像" />}
<p>{ensName ?? `${address?.slice(0, 6)}...${address?.slice(-4)}`}</p>
<p>
{balance?.formatted} {balance?.symbol}
</p>
<p>网络: {chain?.name}</p>
</div>
);
}
链切换
// components/NetworkSwitcher.tsx
import { useAccount, useSwitchChain } from "wagmi";
export function NetworkSwitcher() {
const { chain } = useAccount();
const { chains, switchChain, isPending, error } = useSwitchChain();
return (
<div>
<p>当前: {chain?.name ?? "未连接"}</p>
<div>
{chains.map((c) => (
<button
key={c.id}
onClick={() => switchChain({ chainId: c.id })}
disabled={isPending || c.id === chain?.id}
>
{c.name}
</button>
))}
</div>
{error && <p>错误: {error.message}</p>}
</div>
);
}
交易执行
发送交易
// components/SendTransaction.tsx
import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi";
import { parseEther } from "viem";
export function SendTransaction() {
const { data: hash, isPending, error, sendTransaction } = useSendTransaction();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const to = formData.get("to") as `0x${string}`;
const value = formData.get("value") as string;
sendTransaction({
to,
value: parseEther(value),
});
}
return (
<form onSubmit={handleSubmit}>
<input name="to" placeholder="0x..." required />
<input name="value" placeholder="0.01" required />
<button type="submit" disabled={isPending}>
{isPending ? "发送中..." : "发送"}
</button>
{hash && <p>交易哈希: {hash}</p>}
{isConfirming && <p>确认中...</p>}
{isSuccess && <p>已确认!</p>}
{error && <p>错误: {error.message}</p>}
</form>
);
}
合约交互
// components/ContractInteraction.tsx
import {
useReadContract,
useWriteContract,
useWaitForTransactionReceipt,
} from "wagmi";
import { parseUnits, formatUnits } from "viem";
import { erc20Abi } from "viem";
const TOKEN_ADDRESS = "0x...";
export function TokenBalance({ address }: { address: `0x${string}` }) {
const { data: balance, refetch } = useReadContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "balanceOf",
args: [address],
});
return <p>余额: {balance ? formatUnits(balance, 18) : "0"}</p>;
}
export function TokenTransfer() {
const { data: hash, writeContract, isPending } = useWriteContract();
const { isSuccess } = useWaitForTransactionReceipt({ hash });
function handleTransfer(to: string, amount: string) {
writeContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "transfer",
args: [to as `0x${string}`, parseUnits(amount, 18)],
});
}
return (
<button
onClick={() => handleTransfer("0x...", "100")}
disabled={isPending}
>
{isPending ? "转账中..." : "转账 100 代币"}
</button>
);
}
EIP-712 类型化数据签名
// components/SignTypedData.tsx
import { useSignTypedData, useAccount } from "wagmi";
const domain = {
name: "My dApp",
version: "1",
chainId: 1,
verifyingContract: "0x..." as const,
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
export function SignPermit() {
const { address } = useAccount();
const { signTypedData, data: signature, isPending } = useSignTypedData();
function handleSign() {
signTypedData({
domain,
types,
primaryType: "Permit",
message: {
owner: address!,
spender: "0x..." as const,
value: BigInt("1000000000000000000"),
nonce: BigInt(0),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
},
});
}
return (
<div>
<button onClick={handleSign} disabled={isPending}>
签名授权
</button>
{signature && <p>签名: {signature}</p>}
</div>
);
}
错误处理
// utils/errors.ts
import { BaseError, ContractFunctionRevertedError } from "viem";
export function parseContractError(error: unknown): string {
if (error instanceof BaseError) {
const revertError = error.walk(
(err) => err instanceof ContractFunctionRevertedError
);
if (revertError instanceof ContractFunctionRevertedError) {
const errorName = revertError.data?.errorName ?? "未知错误";
return `合约回滚: ${errorName}`;
}
return error.shortMessage;
}
return "发生未知错误";
}
流程集成
| 流程 | 目的 |
|---|---|
dapp-frontend-development.js |
dApp 构建 |
hd-wallet-implementation.js |
钱包集成 |
multi-signature-wallet.js |
多签 dApp |
最佳实践
- 始终处理连接错误
- 显示交易状态反馈
- 支持多种钱包类型
- 优雅地实现链切换
- 缓存频繁访问的数据
- 使用多个钱包进行测试