在Web3应用开发中,前端与智能合约的交互是核心技能之一。本文将详细介绍如何使用 Web3Modal 连接用户钱包,并通过 Wagmi 库实现与智能合约的高效交互。
我们将从环境配置开始,逐步实现钱包连接、余额查询、代币存款等完整功能。对于想要深入学习的开发者,建议在阅读本文的同时,注册主流交易所的测试账号来验证代码逻辑,对比不同平台的API设计差异。通过入门宝等靠谱的资源平台,可以快速获取各类交易所的注册入口和开发文档,这对理解真实的Web3交易流程非常有帮助。

在开始之前,我们需要了解以下核心库的作用:
Web3Modal
Wagmi
React Query
project/
├── contracts/ # Solidity 合约
├── ignition/ # 部署脚本和地址
├── web/ # 前端项目
│ ├── src/
│ │ ├── generated.ts # Wagmi 自动生成的 hooks
│ │ ├── hooks/ # 自定义 hooks
│ │ └── components/ # React 组件
├── wagmi.config.ts # Wagmi 配置文件
└── hardhat.config.ts # Hardhat 配置Web3Modal 的集成非常简单,官方提供了详细的 接入文档。以下是核心配置:
import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react'
import { mainnet, sepolia } from 'viem/chains'
// 1. 配置项目信息
const projectId = 'YOUR_PROJECT_ID' // 在 WalletConnect 获取
// 2. 配置支持的链
const chains = [mainnet, sepolia] as const
// 3. 创建 Wagmi 配置
const wagmiConfig = defaultWagmiConfig({
chains,
projectId,
metadata: {
name: 'Web3 Exchange',
description: 'Your Web3 Exchange App',
url: 'https://your-domain.com',
icons: ['https://your-domain.com/icon.png']
}
})
// 4. 创建 Web3Modal 实例
createWeb3Modal({ wagmiConfig, projectId, chains })import { useWeb3Modal } from '@web3modal/wagmi/react'
import { useAccount, useDisconnect } from 'wagmi'
function ConnectButton() {
const { open } = useWeb3Modal()
const { address, isConnected } = useAccount()
const { disconnect } = useDisconnect()
if (isConnected) {
return (
<div>
<p>已连接: {address}</p>
<button onClick={() => disconnect()}>断开连接</button>
</div>
)
}
return <button onClick={() => open()}>连接钱包</button>
}在项目根目录创建 wagmi.config.ts,这是自动生成类型安全 hooks 的关键:
import { defineConfig } from "@wagmi/cli";
import { hardhat, react } from "@wagmi/cli/plugins";
import address from "./ignition/deployments/chain-31337/deployed_addresses.json";
export default defineConfig({
// 生成文件的输出路径
out: "web/src/generated.ts",
plugins: [
// Hardhat 插件配置
hardhat({
project: "./",
// 关键:映射合约部署地址
deployments: {
YexiyueToken: address["YexiyueTokenModule#YexiyueToken"] as any,
Exchange: address["Exchange#Exchange"] as any,
},
// 定义构建命令
commands: {
clean: "pnpm hardhat clean",
build: "pnpm hardhat compile",
rebuild: "pnpm hardhat compile",
},
}),
// React 插件:生成 React Hooks
react(),
],
});执行命令生成类型安全的 hooks:
pnpm wagmi generate生成的 generated.ts 文件包含:
useReadContractName* - 读取合约状态useWriteContractName* - 写入合约状态useWatchContractName* - 监听合约事件contractNameAddress - 合约地址常量contractNameABI - 合约 ABI核心优势:
在 Web3 交易所中,我们需要查询多种余额:
import {
exchangeAddress,
useReadExchangeBalanceOf,
useReadYexiyueTokenAllowance,
useReadYexiyueTokenBalanceOf,
yexiyueTokenAddress,
} from "@/generated";
import { useQueryClient } from "@tanstack/react-query";
import { useMemoizedFn } from "ahooks";
import { useBalance } from "wagmi";
// 定义 ETH 的特殊地址
export const ETHER_ADDRESS = "0x0000000000000000000000000000000000000000";
/**
* 自定义 Hook: 查询用户的完整资产状况
* @param address - 用户钱包地址
*/
export const useBalances = ({ address }: { address: `0x${string}` }) => {
const queryClient = useQueryClient();
// 1. 查询钱包 ETH 余额
const { data: etherBalance, queryKey: etherBalanceQueryKey } = useBalance({
address,
});
// 2. 查询钱包 YXT 代币余额
const { data: YXTBalance, queryKey: YXTBalanceQueryKey } =
useReadYexiyueTokenBalanceOf({
args: [address],
});
// 3. 查询交易所中的 ETH 余额
const { data: exchangeETHBalance, queryKey: exchangeETHBalanceQueryKey } =
useReadExchangeBalanceOf({
args: [ETHER_ADDRESS, address],
});
// 4. 查询交易所中的 YXT 余额
const { data: exchangeYXTBalance, queryKey: exchangeYXTBalanceQueryKey } =
useReadExchangeBalanceOf({
args: [yexiyueTokenAddress, address],
});
// 5. 查询 YXT 对交易所的授权额度
const { data: YXTAllowance, queryKey: YXTAllowanceQueryKey } =
useReadYexiyueTokenAllowance({
args: [address, exchangeAddress],
});
/**
* 刷新所有余额查询
* 在交易成功后调用此方法更新 UI
*/
const invalidateQueries = useMemoizedFn(() => {
queryClient.invalidateQueries({ queryKey: etherBalanceQueryKey });
queryClient.invalidateQueries({ queryKey: exchangeETHBalanceQueryKey });
queryClient.invalidateQueries({ queryKey: exchangeYXTBalanceQueryKey });
queryClient.invalidateQueries({ queryKey: YXTBalanceQueryKey });
queryClient.invalidateQueries({ queryKey: YXTAllowanceQueryKey });
});
/**
* 检查是否需要增加授权额度
* @param balance - 计划存入的金额
* @returns [是否需要授权, 需要的总授权额度]
*/
const isOverflowedBalance = useMemoizedFn((balance?: bigint) => {
if (YXTAllowance !== undefined && exchangeYXTBalance !== undefined) {
if (!balance) {
return [exchangeYXTBalance >= YXTAllowance, exchangeYXTBalance] as const;
} else {
const newAllowance = balance + exchangeYXTBalance;
return [newAllowance >= YXTAllowance, newAllowance] as const;
}
}
return [false, 0n] as const;
});
return {
etherBalance,
YXTBalance,
exchangeETHBalance,
exchangeYXTBalance,
invalidateQueries,
isOverflowedBalance,
};
};React Query 缓存管理
queryKey 唯一标识每个查询invalidateQueries 可以精确刷新特定查询BigInt 处理
bigint 类型parseEther 和 formatEther 进行转换授权逻辑
approve 函数存款功能的完整流程:
import {
exchangeAddress,
useWriteExchange,
useWriteYexiyueTokenApprove,
} from "@/generated";
import { ETHER_ADDRESS, useBalances } from "@/hooks/useBalances";
import { App, Button, Form, InputNumber, Modal, Select } from "antd";
import { useEffect, useState } from "react";
import { formatEther, parseEther } from "viem";
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
// 可选的代币列表
const tokenOptions = [
{ label: "ETH", value: ETHER_ADDRESS },
{ label: "YXT", value: yexiyueTokenAddress },
];
export const Deposit = () => {
const [open, setOpen] = useState(false);
const { address } = useAccount();
const { message, modal } = App.useApp();
if (!address) return null;
// 获取余额信息
const {
invalidateQueries,
etherBalance,
YXTBalance,
isOverflowedBalance
} = useBalances({ address });
const [form] = Form.useForm();
const token = Form.useWatch("token", form);
// 存款交易
const {
data: hash,
writeContract: deposit,
isPending,
error
} = useWriteExchange();
// 授权交易
const { writeContractAsync: approve } = useWriteYexiyueTokenApprove();
// 等待交易确认
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
// 交易成功后的处理
useEffect(() => {
if (hash && isSuccess) {
message.success({ content: "存款成功", key: "deposit" });
setOpen(false);
invalidateQueries(); // 刷新余额
form.resetFields();
}
}, [isSuccess, hash]);
// 交易失败处理
useEffect(() => {
if (error && hash) {
message.error({ content: "存款失败", key: "deposit" });
}
}, [error]);
// 提交存款
const onOk = async () => {
const res = await form.validateFields();
if (token === ETHER_ADDRESS) {
// 存入 ETH
deposit({
functionName: "depositEther",
value: parseEther(String(res.amount)),
account: address,
});
} else {
// 存入 ERC20 代币
const [shouldApprove, approveAmount] = isOverflowedBalance(
parseEther(String(res.amount))
);
// 如果授权不足,先进行授权
if (shouldApprove) {
await modal.confirm({
title: "需要授权",
content: `是否授权 ${formatEther(approveAmount)} YXT 给交易所?`,
onOk: async () => {
await approve({
args: [exchangeAddress, approveAmount],
});
},
});
}
deposit({
functionName: "depositToken",
args: [res.token, parseEther(String(res.amount))],
account: address,
});
}
message.loading({
content: "发送交易中...",
key: "deposit",
duration: 0
});
};
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>
存款
</Button>
<Modal
open={open}
onCancel={() => {
setOpen(false);
form.resetFields();
}}
title="存款到交易所"
centered
onOk={onOk}
okText={isPending ? "发送中..." : isLoading ? "确认中..." : "存款"}
okButtonProps={{ loading: isPending || isLoading }}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item
label="存入的代币"
name="token"
rules={[{ required: true, message: "请选择代币" }]}
>
<Select
options={tokenOptions}
placeholder="选择要存入的代币"
/>
</Form.Item>
<Form.Item
name="amount"
label="存入数量"
rules={[
{
required: true,
validator: (_, value) => {
if (!value || value <= 0) {
return Promise.reject("请输入有效金额");
}
// 验证余额是否充足
const amount = parseEther(String(value));
if (token === ETHER_ADDRESS) {
if (etherBalance && amount > etherBalance.value) {
return Promise.reject("ETH 余额不足");
}
} else {
if (YXTBalance && amount > YXTBalance) {
return Promise.reject("YXT 余额不足");
}
}
return Promise.resolve();
},
},
]}
>
<InputNumber
style={{ width: "100%" }}
placeholder="请输入存入数量"
min={0}
step={0.01}
/>
</Form.Item>
</Form>
</Modal>
</>
);
};1. 交易状态管理
const { data: hash, writeContract, isPending, error } = useWriteExchange();
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });isPending: 交易正在发送到钱包签名isLoading: 交易已发送,等待链上确认isSuccess: 交易已被区块链确认2. ETH vs ERC20 的处理差异
// ETH: 直接通过 value 参数发送
deposit({
functionName: "depositEther",
value: parseEther(String(amount)),
});
// ERC20: 需要先授权,再调用 depositToken
await approve({ args: [exchangeAddress, approveAmount] });
deposit({
functionName: "depositToken",
args: [tokenAddress, parseEther(String(amount))],
});3. 授权优化策略
// 检查是否需要新的授权
const [shouldApprove, approveAmount] = isOverflowedBalance(amount);
// 如果当前授权额度不足,计算需要的总额度
// 可以授权更大金额,减少未来的授权次数# 1. 启动本地区块链
pnpm hardhat node
# 2. 部署合约
pnpm hardhat ignition deploy ./ignition/modules/Exchange.ts --network localhost
# 3. 启动前端
cd web && pnpm dev在部署到主网前,建议在测试网进行充分测试:
推荐测试网:
获取测试代币:
真实环境对比: 对于想要深入理解Web3交易流程的开发者,除了测试网之外,注册主流交易所账号进行实际体验也非常重要。通过入门宝等资源平台,可以快速找到各大交易所的注册入口和API文档,对比它们的:
这种对比学习能帮助你更好地理解不同平台的技术选型和产品设计思路。
1. 交易一直 Pending
useWaitForTransactionReceipt 的 timeout 参数2. 合约调用失败
require 条件是否满足3. 授权问题
approve 函数import { BaseError, ContractFunctionRevertedError } from 'viem';
const handleError = (error: Error) => {
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.message;
};// 批量操作减少交易次数
const batchDeposit = async (tokens: Address[], amounts: bigint[]) => {
// 使用 Multicall 合约一次性执行多个操作
await multicall({
contracts: tokens.map((token, i) => ({
address: exchangeAddress,
abi: exchangeABI,
functionName: 'depositToken',
args: [token, amounts[i]],
})),
});
};
// 动态 Gas Price
import { useGasPrice } from 'wagmi';
const { data: gasPrice } = useGasPrice();
// 根据网络拥堵情况调整 Gas Price// 乐观更新:交易发送后立即更新 UI
const optimisticUpdate = () => {
queryClient.setQueryData(
['balance', address],
(old: bigint) => old + depositAmount
);
};
// 交易失败后回滚
const rollbackUpdate = () => {
invalidateQueries();
};本文覆盖了 Web3 前端开发的核心流程:
✅ 钱包连接: Web3Modal 提供统一的钱包接入体验 ✅ 类型安全: Wagmi CLI 自动生成类型安全的 Hooks ✅ 状态管理: React Query 处理异步状态和缓存 ✅ 交易流程: 完整的发送、等待、确认流程 ✅ 错误处理: 优雅处理各种异常情况
想要获取完整的项目代码和深入研究?
📦 GitHub 仓库: Yexiyue-Token
仓库包含:
实践建议:
推荐资源:
Web3 前端开发是一个充满挑战但极具创新性的领域。通过本文的实战演练,相信你已经掌握了从钱包连接到智能合约交互的完整技能链。
记住:最好的学习方式就是动手实践。从简单的余额查询开始,逐步构建更复杂的功能,你将在实践中不断成长。
如果本文对你有帮助,欢迎 Star 项目仓库,也期待在评论区看到你的实践成果!有点过于隐晦了
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。