钱包集成技能Skill wallet-integration

本技能专注于为Web3去中心化应用(dApp)提供专业的钱包连接与交易管理解决方案。它基于wagmi和viem库,实现了多钱包连接器配置、用户友好的连接流程、区块链网络切换、交易执行与Gas估算、EIP-712类型化数据签名、硬件钱包集成以及完善的错误处理。适用于构建需要与以太坊及兼容链交互的前端应用,是DApp开发、DeFi、NFT市场等Web3项目的核心前端组件。关键词:Web3钱包集成,dApp开发,wagmi,viem,区块链前端,以太坊连接,交易管理,EIP-712签名,硬件钱包支持。

DApp开发 0 次安装 0 次浏览 更新于 2/23/2026

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

最佳实践

  1. 始终处理连接错误
  2. 显示交易状态反馈
  3. 支持多种钱包类型
  4. 优雅地实现链切换
  5. 缓存频繁访问的数据
  6. 使用多个钱包进行测试

另请参阅

  • skills/subgraph-indexing/SKILL.md - 数据索引
  • agents/web3-frontend/AGENT.md - 前端专家
  • wagmi 文档
  • viem 文档