介绍
Web3 交易所(前端篇)
# Web3 交易所(前端篇)
在上一篇关于Web3 交易所(合约篇) - 掘金 (juejin.cn) (opens new window)文章之后,本文将深入探讨如何利用 Web3Modal 来连接用户钱包,并通过 Wagmi 库与智能合约进行交互。我们将从配置 Wagmi 开始,逐步介绍如何生成必要的 hooks,并通过实例演示如何读取余额、存款等操作。
# Web3Modal 连接钱包
首先,使用 Web3Modal 连接钱包是一个简单的集成过程。你可以参考Web3Modal 官方教程 (opens new window)来实现这一功能。
# Wagmi 配置与智能合约交互
接下来,我们将详细介绍如何使用 Wagmi 的 hooks 与智能合约进行交互。
# 配置 Wagmi
在项目的根目录下创建wagmi.config.ts
文件,并进行如下配置:
// 导入定义配置的函数
import { defineConfig } from "@wagmi/cli";
// 导入hardhat和react插件
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
通过执行以下命令,Wagmi 将自动生成所需的 hooks:
pnpm wagmi generate
生成的generates.ts
文件中包含了与合约操作相关的多种方法。
# 读取余额示例
以下是使用useBalances
hook 的一个简单示例:
import {
exchangeAddress,
useReadExchangeBalanceOf,
useReadYexiyueTokenAllowance,
useReadYexiyueTokenBalanceOf,
yexiyueTokenAddress,
} from "@/generated";
import { useQueryClient } from "@tanstack/react-query";
import { useMemoizedFn } from "ahooks";
import { useBalance } from "wagmi";
export const ETHER_ADDRESS = "0x0000000000000000000000000000000000000000";
/**
* 使用指定地址查询各种代币和以太币的余额以及授权情况。
* @param { {address: `0x${string}`}} 参数 - 包含要查询余额的地址。
* @returns 返回一个对象,包含以太币余额、YXT代币余额、交易所中的以太币余额、
* 交易所中的YXT代币余额、刷新查询的方法和判断余额是否溢出的方法。
*/
export const useBalances = ({ address }: { address: `0x${string}` }) => {
// 使用react-query获取queryClient实例
const queryClient = useQueryClient();
// 使用wagmi查询以太币余额
const { data: etherBalance, queryKey: etherBalanceQueryKey } = useBalance({
address,
});
// 查询YXT代币余额
const { data: YXTBalance, queryKey: YXTBalanceQueryKey } =
useReadYexiyueTokenBalanceOf({
args: [address],
});
// 查询交易所中的以太币余额
const { data: exchangeETHBalance, queryKey: exchangeETHBalanceQueryKey } =
useReadExchangeBalanceOf({
args: [ETHER_ADDRESS, address],
});
// 查询交易所中的YXT代币余额
const { data: exchangeYXTBalance, queryKey: exchangeYXTBalanceQueryKey } =
useReadExchangeBalanceOf({
args: [yexiyueTokenAddress, address],
});
// 查询地址对交易所的YXT授权额度
const { data: YXTAllowance, queryKey: YXTAllowanceQueryKey } =
useReadYexiyueTokenAllowance({
args: [address, exchangeAddress],
});
// 编写一个memoized函数,用于刷新所有相关查询
const invalidateQueries = useMemoizedFn(() => {
queryClient.invalidateQueries({
queryKey: etherBalanceQueryKey,
});
queryClient.invalidateQueries({
queryKey: exchangeETHBalanceQueryKey,
});
queryClient.invalidateQueries({
queryKey: exchangeYXTBalanceQueryKey,
});
queryClient.invalidateQueries({
queryKey: YXTBalanceQueryKey,
});
queryClient.invalidateQueries({
queryKey: YXTAllowanceQueryKey,
});
});
// 编写一个memoized函数,用于检查余额是否溢出
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,
};
};
# 存款操作
以下是Deposit
组件的示例,它处理用户向交易所存款的流程:
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";
import { tokenOptions } from "./CreateOrder";
/**
* `Deposit` 组件处理用户的存款流程,允许用户向交易所存入代币或 ETH。
* 它管理用户界面,用于选择代币、输入存款金额以及处理交易批准和提交过程。
*/
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();
// 使用写入 Yexiyue 代币批准合约的钩子
const { writeContractAsync: approve } = useWriteYexiyueTokenApprove();
// 使用等待交易回执的钩子
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
// 当交易成功时,更新状态并重置表单
useEffect(() => {
if (hash && isSuccess) {
message.open({
content: "交易成功",
key: "deposit",
type: "success",
});
setOpen(false);
invalidateQueries();
form.resetFields();
}
}, [isSuccess, hash]);
// 当交易失败时,显示错误消息
useEffect(() => {
if (error && hash) {
message.open({
content: "存款失败",
key: "deposit",
type: "error",
});
}
}, [error]);
// 提交存款操作
const onOk = async () => {
// 验证表单字段
const res = await form.validateFields();
// 根据选择的代币处理存款操作
if (token === ETHER_ADDRESS) {
deposit({
functionName: "depositEther",
value: parseEther(String(res.amount)),
account: address,
});
} else {
// 检查是否需要批准交易
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.open({
content: "发送交易中",
key: "deposit",
type: "loading",
duration: 0,
});
};
// 渲染组件
return (
<>
{/* 显示存款按钮 */}
<Button type="primary" onClick={() => setOpen(true)}>
存款
</Button>
{/* 显示存款模态框 */}
<Modal
open={open}
onCancel={() => {
setOpen(false);
form.resetFields();
}}
title="存款"
centered
onOk={onOk}
cancelText="取消"
okText={isPending ? "发送交易中" : isLoading ? "交易确认中" : "存款"}
okButtonProps={{
loading: isPending || isLoading,
}}
destroyOnClose
>
{/* 表单用于选择代币和输入存款数量 */}
<Form form={form} layout="vertical">
{/* 选择存款代币 */}
<Form.Item
label="存入的Token"
name="token"
rules={[
{
required: true,
message: "请选择要存入的Token",
},
]}
>
<Select options={tokenOptions} placeholder="选择要存入的Token" />
</Form.Item>
{/* 输入存款数量 */}
<Form.Item
name="amount"
label="要存入的数量"
rules={[
{
required: true,
validator: (_, value) => {
// 验证输入
// ...
},
},
]}
>
<InputNumber
style={{ width: "100%" }}
placeholder="请输入要存入的数量"
/>
</Form.Item>
</Form>
</Modal>
</>
);
};
# 最后
如果你对本文内容感兴趣,想要获取完整的代码实现和进一步探索,可以访问Yexiyue-Token GitHub 仓库 (opens new window)。在这个仓库中,你将找到所有相关的 Solidity 合约代码、测试脚本及前端代码。