Web3交易所(前端篇)

4,588 阅读5分钟

Web3交易所(前端篇)

在上一篇关于Web3交易所(合约篇) - 掘金 (juejin.cn)文章之后,本文将深入探讨如何利用Web3Modal来连接用户钱包,并通过Wagmi库与智能合约进行交互。我们将从配置Wagmi开始,逐步介绍如何生成必要的hooks,并通过实例演示如何读取余额、存款等操作。

Web3Modal连接钱包

首先,使用Web3Modal连接钱包是一个简单的集成过程。你可以参考Web3Modal 官方教程来实现这一功能。

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

image-20240514165242643.png

生成的generates.ts文件中包含了与合约操作相关的多种方法。

读取余额示例

以下是使用useBalanceshook的一个简单示例:

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仓库。在这个仓库中,你将找到所有相关的Solidity合约代码、测试脚本及前端代码。