DEV Community

Steven
Steven

Posted on

How to integrate ERC20 token presale smart contract with Frontend

Introduction

Integrating an ERC20 token presale smart contract with a frontend using Rainbow Kit is a great way to provide a seamless user experience for token buyers. Rainbow Kit simplifies the wallet connection process and user interface, making it easier for developers to build decentralized applications (dApps). This article will guide you through the steps necessary to set up the integration.
We are going to use Next.js for the frontend development, Ethers.js for smart contract integration and RainbowKit with Wagmi for wallet connections.

The image below is what I am going to create for SPX token presale.

Image description

You can also refer this article for SPX presale smart contract.

How to implement step by step?

1. Create a Next.js project

We are going to use Next.js with Typescript and Tailwind css for the frontend development.

And install the required dependencies.

2. Configure Web3 Providers

Create a new file src/wagmi.ts and configure the chains and 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. Create Provider Utilites and blockchain utilities

  • Create a new file src/utils/web3Providers.ts and write the following code. This code creates a provider instance, handle provider switching, and manage the RPC connections
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
  • Then create a new file src/utils/ethUtils.ts and write the following code. This code has the several blockchain utility functions.
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. Create a context for web3 and custom hooks

  • Create a new file src/contexts/web3Context.tsx and write the following code. This code creates a context for web3 and provides the web3 context to the entire application.
"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
  • Then create a custom hook (src/hooks/useWeb3.ts) to use the web3 context.
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. Combine providers

"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 Integration

  • Connect wallet

This is the code for the connect wallet button.

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

Conclusion

Building a Web3 presale dApp with Next.js, Ethers.js, Wagmi, and RainbowKit creates a powerful and user-friendly platform for token sales. This implementation demonstrates how modern Web3 tools can create sophisticated yet accessible DeFi applications that bridge the gap between blockchain technology and mainstream users.

Top comments (2)

Collapse
 
dodger213 profile image
Mitsuru Kudo

Thank you so much for the helpful information!
Highly recommended.

Collapse
 
btc415 profile image
LovelyBTC

Thansks for sharing the detailed step to integrate smart contract with DApp frontend.
Thanks again