如何将 ERC20 代币预售智能合约与前端集成

2025-06-10

如何将 ERC20 代币预售智能合约与前端集成

介绍

使用 Rainbow Kit 将 ERC20 代币预售智能合约与前端集成,是为代币购买者提供无缝用户体验的绝佳方式。Rainbow Kit 简化了钱包连接流程和用户界面,使开发者能够更轻松地构建去中心化应用程序 (dApp)。本文将指导您完成集成所需的步骤。
我们将使用 Next.js 进行前端开发,使用 Ethers.js 进行智能合约集成,并使用 RainbowKit 和 Wagmi 进行钱包连接。

下面的图片是我将为 SPX 代币预售创建的。

图片描述

您还可以参考本文了解 SPX 预售智能合约。

如何逐步实施?

1. 创建 Next.js 项目

我们将使用 Next.js 与 Typescript 和 Tailwind css 进行前端开发。

并安装所需的依赖项。

2. 配置 Web3 提供程序

创建新文件src/wagmi.ts并配置链和projectId。

import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { mainnet, sepolia } from "wagmi/chains";

export const config = getDefaultConfig({
  appName: "IRONWILL",
  projectId: "68449b069bf507d7dc50e5c1bcb82c50", // Replace with your actual project ID
  chains: [mainnet, sepolia],
  ssr: true, // Enable server-side rendering support
});
Enter fullscreen mode Exit fullscreen mode

3. 创建提供商实用程序和区块链实用程序

  • 创建一个新文件src/utils/web3Providers.ts并编写以下代码。此代码用于创建提供程序实例、处理提供程序切换以及管理 RPC 连接
import { useMemo } from 'react';
import {
  FallbackProvider,
  JsonRpcProvider,
  BrowserProvider,
  JsonRpcSigner,
} from 'ethers';
import type { Account, Chain, Client, Transport } from 'viem';
import { type Config, useClient, useConnectorClient } from 'wagmi';

export function clientToProvider(client: Client<Transport, Chain>) {
  const { chain, transport } = client;
  const network = {
    chainId: chain.id,
    name: chain.name,
    ensAddress: chain.contracts?.ensRegistry?.address,
  };
  if (transport.type === 'fallback') {
    const providers = (transport.transports as ReturnType<Transport>[]).map(
      ({ value }) => new JsonRpcProvider(value?.url, network)
    );
    if (providers.length === 1) return providers[0];
    return new FallbackProvider(providers);
  }
  return new JsonRpcProvider(transport.url, network);
}

export function useEthersProvider({ chainId }: { chainId?: number } = {}) {
  const client = useClient<Config>({ chainId })!;
  return useMemo(() => clientToProvider(client), [client]);
}

export function clientToSigner(client: Client<Transport, Chain, Account>) {
  const { account, chain, transport } = client;
  const network = {
    chainId: chain.id,
    name: chain.name,
    ensAddress: chain.contracts?.ensRegistry?.address,
  };
  const provider = new BrowserProvider(transport, network);
  const signer = new JsonRpcSigner(provider, account.address);
  return signer;
}

export async function useEthersSigner({ chainId }: { chainId?: number } = {}) {
  const { data: client } = useConnectorClient<Config>({ chainId });
  return useMemo(() => (client ? clientToSigner(client) : undefined), [client]);
}

Enter fullscreen mode Exit fullscreen mode
  • 然后创建一个新文件src/utils/ethUtils.ts并写入以下代码。此代码包含几个区块链实用函数。
import { ethers } from "ethers";
import { toast } from "react-toastify";

const ERC20_ABI = [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
];

export const TOKENS = {
  // sepolia
  USDT: "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0",
  USDC: "0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8",
  DAI: "0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357",
  SPX: "0x60081fa3c771BA945Aa3E2112b1f557D80e88575",
};

export async function getBalances(
  address: string
): Promise<{ [key: string]: string }> {
  const provider = new ethers.JsonRpcProvider(
    process.env.NEXT_PUBLIC_INFURA_URL
  );

  const balances: { [key: string]: string } = {};

  try {
    // Get ETH balance
    const ethBalance = await provider.getBalance(address);
    balances.ETH = ethers.formatEther(ethBalance);

    // Get token balances
    for (const [symbol, tokenAddress] of Object.entries(TOKENS)) {
      const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
      const balance = await contract.balanceOf(address);
      const decimals = await contract.decimals();
      balances[symbol] = ethers.formatUnits(balance, decimals);
    }

    return balances;
  } catch (error) {
    console.error("Error fetching balances:", error);
    toast.error("Error fetching balances");
    return {};
  }
}
Enter fullscreen mode Exit fullscreen mode

4. 为 web3 和自定义 hooks 创建上下文

  • 创建一个新文件src/contexts/web3Context.tsx并写入以下代码。此代码为 web3 创建一个上下文,并将 web3 上下文提供给整个应用程序。
"use client";

import {createContext, useCallback, useEffect, useMemo, useState} from "react";
import Web3 from "web3";
import { useAccount, useChainId } from "wagmi";
import { Contract, ContractRunner, ethers } from "ethers";

import SPXTokenContractABI from "@/abis/SPXTokenContractABI.json";
import PresaleContractABI from "@/abis/PresaleContractABI.json";
import { useEthersProvider, useEthersSigner } from "@/utils/web3Providers";
import {defaultRPC, SPXTokenContractAddress, PresaleContractAddress } from "@/data/constants";
import { Web3ContextType } from "@/types";

declare let window: any;

let web3: any;

const Web3Context = createContext<Web3ContextType | null>(null);

export const Web3Provider = ({ children }: { children: React.ReactNode }) => {
  const { address, isConnected } = useAccount();
  const chainId = useChainId();
  const signer = useEthersSigner();
  const ethersProvider = useEthersProvider();
  let defaultProvider: any;
  if (chainId === 1) {
    defaultProvider = new ethers.JsonRpcProvider(defaultRPC.mainnet);
  } else if (chainId === 11155111) {
    defaultProvider = new ethers.JsonRpcProvider(defaultRPC.sepolia);
  }

  const [provider, setProvider] = useState<ContractRunner>(defaultProvider);
  const [SPXTokenContract, setSPXTokenContract] = useState<Contract>(
    {} as Contract
  );
  const [presaleContract, setPresaleContract] = useState<Contract>(
    {} as Contract
  );
  const [spxTokenAddress, setSPXTokenAddress] = useState<string>("");

  const init = useCallback(async () => {
    try {
      if (typeof window !== "undefined") {
        web3 = new Web3(window.ethereum);
      }
      if (!isConnected || !ethersProvider) {
        // console.log("Not connected wallet");
      } else {
        setProvider(ethersProvider);
        // console.log("Connected wallet");
      }

      if (chainId === 1) {
        const _spxTokenContractWeb3: any = new web3.eth.Contract(
          SPXTokenContractABI,
          SPXTokenContractAddress.mainnet
        );
        const _presaleContractWeb3: any = new web3.eth.Contract(
          PresaleContractABI,
          PresaleContractAddress.mainnet
        );
        setSPXTokenContract(_spxTokenContractWeb3);
        setPresaleContract(_presaleContractWeb3);
        setSPXTokenAddress(SPXTokenContractAddress.mainnet);
      } else if (chainId === 11155111) {
        const _spxTokenContractWeb3: any = new web3.eth.Contract(
          SPXTokenContractABI,
          SPXTokenContractAddress.sepolia
        );
        const _presaleContractWeb3: any = new web3.eth.Contract(
          PresaleContractABI,
          PresaleContractAddress.sepolia
        );
        setSPXTokenContract(_spxTokenContractWeb3);
        setPresaleContract(_presaleContractWeb3);
        setSPXTokenAddress(SPXTokenContractAddress.sepolia);
      }
    } catch (err) {
      console.log(err);
    }
  }, [isConnected, ethersProvider, chainId]);

  useEffect(() => {
    init();
  }, [init]);

  const value = useMemo(
    () => ({
      account: address,
      chainId,
      isConnected,
      library: provider ?? signer,
      SPXTokenContract,
      presaleContract,
      spxTokenAddress,
      web3,
    }),
    [
      address,
      chainId,
      isConnected,
      provider,
      signer,
      SPXTokenContract,
      presaleContract,
      spxTokenAddress,
    ]
  );

  return <Web3Context.Provider value={value}>{children}</Web3Context.Provider>;
};

export default Web3Context;
Enter fullscreen mode Exit fullscreen mode
  • 然后创建一个自定义钩子(src/hooks/useWeb3.ts)来使用 web3 上下文。
import { useContext } from "react";
import Web3Context from "@/contexts/web3Context";

const useWeb3 = () => {
  const context = useContext(Web3Context);
  if (!context) throw new Error("context must be use inside provider");
  return context;
};

export default useWeb3;
Enter fullscreen mode Exit fullscreen mode

5. 合并提供商

"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
import { config } from "@/wagmi";
import "@rainbow-me/rainbowkit/styles.css";
import { Web3Provider } from "@/contexts/web3Context";
const client = new QueryClient();

export default function RootProvider({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={client}>
        <RainbowKitProvider
          coolMode={true}
          theme={darkTheme({
            accentColor: "#824B3D",
            accentColorForeground: "#dbdbcf",
            borderRadius: "small",
          })}
        >
          <Web3Provider>{children}</Web3Provider>
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

6. Web3 集成

  • 连接钱包

这是连接钱包按钮的代码。

import { ConnectButton } from "@rainbow-me/rainbowkit";

const PreSale: React.FC = () => {

  ....

 const handlePayAmountChange = async (
    e: React.ChangeEvent<HTMLInputElement> | { target: { value: string } }
  ) => {
    if (preSaleStage !== PreSaleStage.Running) return;

    const value = e.target.value;
    const regex = /^\d*\.?\d{0,6}$/;
    if (!regex.test(value)) {
      return;
    }

    if (value === "" || value === "0") {
      setPayAmount(0);
      setTokenAmount(0);
    } else {
      try {
        const newPayAmount = ethers.parseUnits(value, 6);
        setPayAmount(parseFloat(value));
        if (paymentType === "ETH") {
          const ethAmount = ethers.parseEther(value);
          const temp = await presaleContract.methods
            .estimatedTokenAmountAvailableWithETH(ethAmount.toString())
            .call();
          const expectedTokenAmount = ethers.formatUnits(temp, 18);
          setTokenAmount(parseFloat(expectedTokenAmount));
        } else {
          const temp = await presaleContract.methods
            .estimatedTokenAmountAvailableWithCoin(
              newPayAmount.toString(),
              TOKENS[paymentType as keyof typeof TOKENS]
            )
            .call();
          const expectedTokenAmount =
            paymentType === "DAI"
              ? ethers.formatUnits(temp, 6)
              : ethers.formatUnits(temp, 18);
          setTokenAmount(parseFloat(expectedTokenAmount));
        }
      } catch (error) {
        console.error("Error fetching token amount:", error);
        toast.error("Error fetching token amount");
        setTokenAmount(0);
      }
    }
  };

    const handleBuy = async () => {
    setPayAmount(0);
    setTokenAmount(0);
    try {
      switch (paymentType) {
        case "ETH":
          const ethAmount = ethers.parseEther(payAmount.toString());
          const txETH = await presaleContract.methods
            .buyWithETH()
            .send({ from: account, value: ethAmount.toString() });
          break;
        case "USDT":
          const txUSDT = await presaleContract.methods
            .buyWithUSDT(toBigInt(tokenAmount.toString()))
            .send({ from: account });
          break;
        case "USDC":
          const txUSDC = await presaleContract.methods
            .buyWithUSDC(toBigInt(tokenAmount.toString()))
            .send({ from: account });
          break;
        case "DAI":
          const txDAI = await presaleContract.methods
            .buyWithDAI(toBigInt(tokenAmount.toString()))
            .send({ from: account });
          break;
      }

      const balance = await presaleContract.methods
        .getTokenAmountForInvestor(account)
        .call();
      const formattedBalance = ethers.formatUnits(balance, 18);
      const tempFundsRaised = await presaleContract.methods
        .getFundsRaised()
        .call();
      const tempTokensAvailable = await presaleContract.methods
        .tokensAvailable()
        .call();
      const formattedTokensAvailable = parseFloat(
        ethers.formatUnits(tempTokensAvailable, 18)
      );

      fetchBalances();
      setFundsRaised(parseFloat(tempFundsRaised) / 1e6);
      setTokensAvailable(Math.floor(formattedTokensAvailable));
      setClaimableSPXBalance(formattedBalance);
    } catch (error) {
      toast.error("Transaction failed. Please try again.");
    }
  };

  ...

  return {
    <button
      className="max-w-[70%] w-full bg-[#824B3D] p-3 rounded font-bold mb-4 hover:bg-orange-800 truncate"
      onClick={account ? openAccountModal : openConnectModal}
    >
      {account ? account.displayName : "CONNECT WALLET"}
    </button>

    {account && (
    <button
      className="w-full bg-[#824B3D] p-3 rounded font-bold mb-4 hover:bg-orange-800 disabled:bg-[#333] disabled:cursor-not-allowed truncate"
      disabled={
        isLoading ||
        preSaleStage === PreSaleStage.Ready ||
        preSaleStage === PreSaleStage.Ended ||
        (preSaleStage === PreSaleStage.Running &&
          (payAmount < min ||
            payAmount >
              parseFloat(balances[paymentType]))) ||
        (preSaleStage === PreSaleStage.Claimable &&
          parseFloat(claimableSPXBalance) < 1)
      }
      onClick={
        preSaleStage !== PreSaleStage.Claimable
          ? handleBuy
          : handleClaim
      }
    >
      {preSaleStage < PreSaleStage.Ended
        ? "BUY"
        : "CLAIM"}
    </button>
  )}

  ....
}
Enter fullscreen mode Exit fullscreen mode

结论

使用 Next.js、Ethers.js、Wagmi 和 RainbowKit 构建 Web3 预售 dApp,打造一个功能强大且用户友好的代币销售平台。此实现展示了现代 Web3 工具如何创​​建复杂且易于访问的 DeFi 应用,从而弥合区块链技术与主流用户之间的差距。

链接链接:https://dev.to/stevendev0822/how-to-integrate-erc20-token-presale-smart-contract-with-frontend-3mjj
PREV
在生产环境中使用 Golang 并发应用程序概述。采取的步骤步骤 2:客户端实现(用于 HTTP 调用)结论
NEXT
构建专业的 Solana 钱包追踪 Telegram 机器人