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.
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
});
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]);
}
- 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 {};
}
}
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;
- 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;
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>
);
}
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>
)}
....
}
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 (19)
This article is incredibly insightful and packed with valuable information in ERC20 token presale smart contract integration with dApp frontend! It has provided me with practical strategies that I can apply directly to my work. Highly recommend it to anyone looking to enhance their knowledge and skills in Blockchain development.
Thanks for your article.
Looks nice like always.
👍👍👍
I am new to here, but it feels like I am with senior developers.
It arises me new interest in blockchain technology.
Thank you Steven.
Thanks for your article.
Deeply impressive.
I am new to blockchain, but this article gave me comprehensive guide to ERC20 token presale smart contract integration with dApp frontend
Thank you so much for the helpful information!
Highly recommended.
Thansks for sharing the detailed step to integrate smart contract with DApp frontend.
Thanks again
Really impressive.
Thank you for sharing.
Looks amazing.
Thanks.
Good article.