首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >开发Web3交易所前端开发指南-从注册测试到Wagmi实战的完整流程

开发Web3交易所前端开发指南-从注册测试到Wagmi实战的完整流程

原创
作者头像
用户11900274
发布2025-11-24 18:22:45
发布2025-11-24 18:22:45
10
举报

前言

在Web3应用开发中,前端与智能合约的交互是核心技能之一。本文将详细介绍如何使用 Web3Modal 连接用户钱包,并通过 Wagmi 库实现与智能合约的高效交互。

我们将从环境配置开始,逐步实现钱包连接、余额查询、代币存款等完整功能。对于想要深入学习的开发者,建议在阅读本文的同时,注册主流交易所的测试账号来验证代码逻辑,对比不同平台的API设计差异。通过入门宝等靠谱的资源平台,可以快速获取各类交易所的注册入口和开发文档,这对理解真实的Web3交易流程非常有帮助。


一、技术栈介绍

1.1 核心依赖

在开始之前,我们需要了解以下核心库的作用:

Web3Modal

  • 提供统一的钱包连接界面
  • 支持 MetaMask、WalletConnect、Coinbase Wallet 等主流钱包
  • 优秀的用户体验和跨平台兼容性

Wagmi

  • React Hooks 形式的 Web3 开发库
  • 类型安全,基于 TypeScript
  • 自动管理连接状态和交易生命周期
  • 与 Viem 深度集成,性能优异

React Query

  • Wagmi 底层依赖,用于状态管理
  • 提供缓存、重试、乐观更新等特性

1.2 项目结构

代码语言:javascript
复制
project/
├── contracts/              # Solidity 合约
├── ignition/              # 部署脚本和地址
├── web/                   # 前端项目
│   ├── src/
│   │   ├── generated.ts   # Wagmi 自动生成的 hooks
│   │   ├── hooks/         # 自定义 hooks
│   │   └── components/    # React 组件
├── wagmi.config.ts        # Wagmi 配置文件
└── hardhat.config.ts      # Hardhat 配置

二、Web3Modal 钱包连接

2.1 快速集成

Web3Modal 的集成非常简单,官方提供了详细的 接入文档。以下是核心配置:

代码语言:javascript
复制
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 })

2.2 在组件中使用

代码语言:javascript
复制
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 配置与 Hooks 生成

3.1 配置文件详解

在项目根目录创建 wagmi.config.ts,这是自动生成类型安全 hooks 的关键:

代码语言:javascript
复制
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(),
  ],
});

3.2 自动生成 Hooks

执行命令生成类型安全的 hooks:

代码语言:javascript
复制
pnpm wagmi generate

生成的 generated.ts 文件包含:

  • useReadContractName* - 读取合约状态
  • useWriteContractName* - 写入合约状态
  • useWatchContractName* - 监听合约事件
  • contractNameAddress - 合约地址常量
  • contractNameABI - 合约 ABI

核心优势:

  • 完全类型安全,编译时捕获错误
  • 自动推断函数参数和返回值类型
  • 无需手动维护 ABI 和地址

四、实战:余额查询功能

4.1 设计思路

在 Web3 交易所中,我们需要查询多种余额:

  • 钱包中的 ETH 余额
  • 钱包中的代币余额(如 YXT)
  • 交易所合约中托管的 ETH 余额
  • 交易所合约中托管的代币余额
  • 代币对交易所的授权额度

4.2 完整实现

代码语言:javascript
复制
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,
  };
};

4.3 关键技术点

React Query 缓存管理

  • queryKey 唯一标识每个查询
  • invalidateQueries 可以精确刷新特定查询
  • 自动去重和请求合并

BigInt 处理

  • 区块链金额使用 bigint 类型
  • 避免 JavaScript Number 的精度问题
  • 使用 parseEtherformatEther 进行转换

授权逻辑

  • ERC20 代币需要先授权才能被合约转移
  • 授权额度不足时需要调用 approve 函数
  • 可以一次性授权较大额度,减少交易次数

五、实战:存款功能实现

5.1 业务流程

存款功能的完整流程:

  1. 用户选择存入的代币(ETH 或 ERC20)
  2. 输入存款金额
  3. 如果是 ERC20 且授权不足,先进行授权
  4. 调用合约的存款函数
  5. 等待交易确认
  6. 更新余额显示

5.2 完整代码

代码语言:javascript
复制
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>
    </>
  );
};

5.3 技术细节解析

1. 交易状态管理

代码语言:javascript
复制
const { data: hash, writeContract, isPending, error } = useWriteExchange();
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
  • isPending: 交易正在发送到钱包签名
  • isLoading: 交易已发送,等待链上确认
  • isSuccess: 交易已被区块链确认

2. ETH vs ERC20 的处理差异

代码语言:javascript
复制
// 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. 授权优化策略

代码语言:javascript
复制
// 检查是否需要新的授权
const [shouldApprove, approveAmount] = isOverflowedBalance(amount);

// 如果当前授权额度不足,计算需要的总额度
// 可以授权更大金额,减少未来的授权次数

六、测试与调试

6.1 本地测试环境

代码语言:javascript
复制
# 1. 启动本地区块链
pnpm hardhat node

# 2. 部署合约
pnpm hardhat ignition deploy ./ignition/modules/Exchange.ts --network localhost

# 3. 启动前端
cd web && pnpm dev

6.2 主网测试建议

在部署到主网前,建议在测试网进行充分测试:

推荐测试网:

  • Sepolia (以太坊测试网)
  • Goerli (即将废弃,但仍可用)
  • Mumbai (Polygon 测试网)

获取测试代币:

  • 通过各测试网的 Faucet 获取测试 ETH
  • 部署测试版本的 ERC20 代币

真实环境对比: 对于想要深入理解Web3交易流程的开发者,除了测试网之外,注册主流交易所账号进行实际体验也非常重要。通过入门宝等资源平台,可以快速找到各大交易所的注册入口和API文档,对比它们的:

  • 钱包连接方式
  • 交易确认流程
  • Gas 费用优化策略
  • 用户体验设计

这种对比学习能帮助你更好地理解不同平台的技术选型和产品设计思路。

6.3 常见问题排查

1. 交易一直 Pending

  • 检查 Gas Price 是否过低
  • 查看区块浏览器确认交易状态
  • 使用 useWaitForTransactionReceipttimeout 参数

2. 合约调用失败

  • 检查合约地址是否正确
  • 验证函数参数类型和值
  • 查看合约的 require 条件是否满足

3. 授权问题

  • 确认已调用 approve 函数
  • 检查授权额度是否足够
  • 注意授权是针对特定合约地址的

七、进阶优化

7.1 错误处理增强

代码语言:javascript
复制
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;
};

7.2 Gas 费用优化

代码语言:javascript
复制
// 批量操作减少交易次数
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

7.3 用户体验优化

代码语言:javascript
复制
// 乐观更新:交易发送后立即更新 UI
const optimisticUpdate = () => {
  queryClient.setQueryData(
    ['balance', address],
    (old: bigint) => old + depositAmount
  );
};

// 交易失败后回滚
const rollbackUpdate = () => {
  invalidateQueries();
};

八、总结与资源

8.1 核心要点回顾

本文覆盖了 Web3 前端开发的核心流程:

钱包连接: Web3Modal 提供统一的钱包接入体验 ✅ 类型安全: Wagmi CLI 自动生成类型安全的 Hooks ✅ 状态管理: React Query 处理异步状态和缓存 ✅ 交易流程: 完整的发送、等待、确认流程 ✅ 错误处理: 优雅处理各种异常情况

8.2 完整代码获取

想要获取完整的项目代码和深入研究?

📦 GitHub 仓库: Yexiyue-Token

仓库包含:

  • 完整的 Solidity 智能合约
  • Hardhat 测试和部署脚本
  • React 前端完整代码
  • 详细的配置文件和文档

8.3 持续学习建议

实践建议:

  1. 从测试网开始,熟悉完整流程后再上主网
  2. 对比学习,注册不同交易所账号体验产品设计(入门宝提供了便捷的注册入口汇总)
  3. 阅读源码,研究 Uniswap、Aave 等知名项目的实现
  4. 参与社区,加入 Discord/Telegram 与开发者交流

推荐资源:


结语

Web3 前端开发是一个充满挑战但极具创新性的领域。通过本文的实战演练,相信你已经掌握了从钱包连接到智能合约交互的完整技能链。

记住:最好的学习方式就是动手实践。从简单的余额查询开始,逐步构建更复杂的功能,你将在实践中不断成长。

如果本文对你有帮助,欢迎 Star 项目仓库,也期待在评论区看到你的实践成果!有点过于隐晦了

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 一、技术栈介绍
      • 1.1 核心依赖
      • 1.2 项目结构
    • 二、Web3Modal 钱包连接
      • 2.1 快速集成
      • 2.2 在组件中使用
    • 三、Wagmi 配置与 Hooks 生成
      • 3.1 配置文件详解
      • 3.2 自动生成 Hooks
    • 四、实战:余额查询功能
      • 4.1 设计思路
      • 4.2 完整实现
      • 4.3 关键技术点
    • 五、实战:存款功能实现
      • 5.1 业务流程
      • 5.2 完整代码
      • 5.3 技术细节解析
    • 六、测试与调试
      • 6.1 本地测试环境
      • 6.2 主网测试建议
      • 6.3 常见问题排查
    • 七、进阶优化
      • 7.1 错误处理增强
      • 7.2 Gas 费用优化
      • 7.3 用户体验优化
    • 八、总结与资源
      • 8.1 核心要点回顾
      • 8.2 完整代码获取
      • 8.3 持续学习建议
    • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档