如何将 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
});
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]);
}
- 然后创建一个新文件
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 {};
}
}
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;
- 然后创建一个自定义钩子(
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;
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>
);
}
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>
)}
....
}
结论
使用 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