Build a full stack token sale contract with Solidity, React and Wagmi hooks
Ever dreamed of launching your own token sale?
Do you want to launch your own token and sell it maybe? Do you want to do an ICO? Do you have some programming experience? Read this guide to build your own token on Ethereum and start selling it to people with your own smart contract and website.
This comprehensive guide will equip you with the knowledge to build a full-fledged token sale dapp from scratch! Utilize the power of Solidity, Hardhat, React, and Wagmi hooks to craft a user-friendly platform where participants can seamlessly acquire your tokens.
## We'll delve into the intricacies of the following:
Solidity Smart Contract Development: We'll meticulously dissect the provided Solidity contract, explaining each function's purpose and showcasing its functionality within the token sale ecosystem. Hardhat for Development and Deployment: Leverage Hardhat's robust features to streamline your development workflow. We'll explore testing, compilation, and deployment strategies to ensure a smooth launch. Building a Frontend with React and Wagmi Hooks: Craft an intuitive user interface for your token sale dapp using React. Wagmi hooks will simplify your interactions with the blockchain, enabling a seamless user experience.
## By the end of this tutorial, you'll be proficient in:
Deploying a secure and feature-rich token sale dapp. Managing whitelist and public sales. Setting token price and purchase limits. Integrating a user-friendly interface for buying tokens.
So, buckle up and get ready to embark on your token sale development journey!
# Installation
We are going to create an empty folder, and run the following command on the terminal to install Hardhat => npm install --save-dev hardhat
Then, with npx hardhat init
command, we will start a Hardhat project. For this project, we will use Javascript.
After the project has ben initiated, we will install these following packages also => npm install @openzeppelin/contracts
# Smart contracts
In this section, we will write two smart contracts: AWToken.sol and TokenSale.sol. AWToken.sol will be an ERC-20 token, and the sale logic will be defined in TokenSale.sol.
## AWToken.sol
```js// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@openzeppelin/contracts/access/Ownable.sol";
contract AWToken is ERC20, Ownable { constructor() ERC20("AWToken", "AWT") Ownable(msg.sender) { _mint(msg.sender, 100 10 * 18); }}```
This is a basic ERC-20 token using Openzeppelin. It is also ownable and the owner is the deployer. Creates 100 AWtokens.
## TokenSale.sol
We start the contract as follows:
```js// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "@openzeppelin/contracts/access/Ownable.sol";import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract TokenSale is Ownable, ReentrancyGuard {}```
As you see, the contract is ownable, and we're using a reentrancy guard to prevent reentrancy attacks.
Inside the contract, let's define the variables and write the constructor.
```js IERC20 public awtoken;
uint256 public tokenPrice; uint256 public tokenSold; //max amount of tokens that can be bought by an address uint8 public maxSale; uint8 public minSale;
mapping(address => uint256) public balances; mapping(address => bool) public whitelistedAddresses;
bool public saleActive = false; bool public whitelistSaleActive = true; bool public paused = false; //events event TokenSold(address indexed buyer, uint256 amount, uint256 valueInUSDT);
constructor(IERC20 _awtoken, uint256 _tokenPrice) Ownable(msg.sender) { awtoken = _awtoken; tokenPrice = _tokenPrice; tokenSold = 0; maxSale = 3; minSale = 1; }```
When you inspect the code above, you'll see we define the things we need for the sale. Such as maxSale, whitelisted addressess, is the sale active etc.
Here, in this contract we add some sort of whitelisting functionality via the usage of mappings. You might not necessarily want to do so. I have written another guide on how to implement whitelisting logic using Merkle Trees. But in this guide, we will use mappings and structure our code accordingly.
### View functions
View functions do not require much explanation. They do not cost gas and can be called by anyone.
```js //view functions
function isWhitelisted(address _address) public view returns (bool) { return whitelistedAddresses[_address]; }
function isSaleActive() public view returns (bool) { return saleActive; }
function isWhitelistSaleActive() public view returns (bool) { return whitelistSaleActive; }
function getTokenPrice() public view returns (uint256) { return tokenPrice; }
function getTokenSold() public view returns (uint256) { return tokenSold; }
function getBalance() public view returns (uint256) { return address(this).balance; }
function getMaxSale() public view returns (uint8) { return maxSale; }
function getMinSale() public view returns (uint8) { return minSale; }
function getPaused() public view returns (bool) { return paused; }
function getOwner() public view returns (address) { return owner(); }```
### Write functions
This is where the magic happens. Let's start with the easier parts.
```js //write functions function setSaleLimits(uint8 _maxSale, uint8 _minSale) external onlyOwner { maxSale = _maxSale; minSale = _minSale; }
function toggleWhitelistSaleActive() external onlyOwner { whitelistSaleActive = !whitelistSaleActive; }
function toggleSaleActive() external onlyOwner { saleActive = !saleActive; }
function addToWhitelist(address _address) external onlyOwner { whitelistedAddresses[_address] = true; }
function removeFromWhitelist(address _address) external onlyOwner { delete whitelistedAddresses[_address]; }
function togglePause() external onlyOwner { paused = !paused; }
function setTokenPrice(uint256 _tokenPrice) external onlyOwner { tokenPrice = _tokenPrice; }```
In setSaleLimits, which is external and onlyOwner, which means that only the owner can call this function, and not by a function inside this contract. we use uint8 for min and max sale because they are going to be small numbers.
Toggle functions are quite self-explanatory. Whatever has the onlyOwner keyword can only be called by the owner, in this case, the deployer of the contract.
addToWhiteList and removeFromWhitelist functions take the address of the user to be whitelisted. As mentioned above, there are other ways like Merkle Trees to deal with whitelisting.
Now let's see the real action.
```js function buyWhitesaleTokens(uint256 _amount) external payable nonReentrant { require(whitelistSaleActive, "WhiteSale is not active"); require(paused == false, "Contract is paused"); require( saleActive == false, "Standard should be inactive during the whitesale" ); require(whitelistedAddresses[msg.sender], "You are not whitelisted"); require( _amount >= minSale && _amount <= maxSale, "Amount must be between 1 and 3" ); require( msg.value >= _amount tokenPrice, "You have to pay the correct amount" ); require( address(this).balance >= _amount tokenPrice, "Not enough ETH" ); require( balances[msg.sender] + _amount <= maxSale, "You cannot buy more than the specified max tokens" );
awtoken.transfer(msg.sender, _amount); balances[msg.sender] += _amount; tokenSold += _amount; }```
This function is only for whitelisted addresses. It is external, therefore cannot be called from another function inside this contract.
It takes the amount of tokens that are wanted to be bought. payable keyword means that this function accepts ETH transfers. we use the reentrancy guard here for security.
msg.value is the amount sent by the function caller. After checking the requirements, we're invoking the awtoken
contract, because we need to send tokens from/by there.
msg.sender is the entity/address that called this function.
```js
function buyTokens(uint256 _amount) external payable nonReentrant { require(saleActive, "Standard sale is not active"); require(paused == false, "Contract is paused"); require(whitelistSaleActive == false, "WhiteSale should not be active"); require( _amount >= minSale && _amount <= maxSale, "Amount must be between 1 and 3" ); require( msg.value >= _amount tokenPrice, "You have to pay the correct amount" ); require( address(this).balance >= _amount tokenPrice, "Not enough ETH" ); require( balances[msg.sender] + _amount <= maxSale, "You cannot buy more than the specified max tokens" );
awtoken.transfer(msg.sender, _amount); balances[msg.sender] += _amount;
tokenSold += _amount; }```
This function is pretty much the same as the previous one, only for non-whitelisted addresses after the whitelist ends.
To finish up the contract, we have these:
```js function withdrawEth() external onlyOwner { payable(msg.sender).transfer(address(this).balance); }
function withdrawToken() external onlyOwner { awtoken.transfer(msg.sender, address(this).balance); }
fallback() external payable { }
receive() external payable { }```
Only owner can withdraw funds and any ETH sent to the contract directly is just accepted as it is, no function is called.
# Deployment
New Hardhat uses Ignition module. In ignition/modules create AWTokenDeployer.js and TokensaleDeployer.js
We need to deploy AWToken first as Token Sale contract needs it...
#### AWTokenDeployer
```jsconst { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
module.exports = buildModule("AWToken", (m) => { const awToken = m.contract("AWToken");
// If you need to call any functions after deployment, you can do that here // m.call(awToken, "someFunction", [...args]);
return { awToken };});```
#### TokensaleDeployer
```jsconst { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");//you'll get and fill this awTokenAddress after deploying the awtoken contractconst awTokenAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
module.exports = buildModule("TokenSale", (m) => { const awToken = m.getParameter("awToken", awTokenAddress);
const tokenSale = m.contract("TokenSale", [awToken, 5]);
// If you need to call any functions after deployment, you can do that here // m.call(tokenSale, "someFunction", [...args]);
return { tokenSale };});```
We give any data we need to pass to the constructor here as you see.
#### hardhat configuration
Go to hardhat.config.js at root directory.
```jsrequire("@nomicfoundation/hardhat-toolbox");
// Ensure your configuration variables are set before executing the scriptconst { vars } = require("hardhat/config");
// Go to https://alchemy.com, sign up, create a new App in// its dashboard, and add its key to the configuration variables
// const ALCHEMY_API_KEY = vars.get("ALCHEMY_API_KEY");
// Add your Sepolia account private key to the configuration variables// To export your private key from Coinbase Wallet, go to// Settings > Developer Settings > Show private key// To export your private key from Metamask, open Metamask and// go to Account Details > Export Private Key// Beware: NEVER put real Ether into testing accounts
// const SEPOLIA_PRIVATE_KEY = vars.get("SEPOLIA_PRIVATE_KEY");
module.exports = { solidity: "0.8.24", networks: { // sepolia: { // url: https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}
, // accounts: [SEPOLIA_PRIVATE_KEY], // }, },};```
This script is an example for deploying to a live network. Here, wyou see Alchemy, and Sepolia testnet. We're not going to use any of them in this guide, so they're commented out. We're going to use a local node.
On terminal run npx hardhat node
which will fire the local blockchain. Keep that terminal tab open, go to a different one and run the following : npx hardhat ignition deploy ./ignition/modules/AWTokenDeployer.js --network
localhost
You should get something like this in return:
```bashCompiled 9 Solidity files successfully (evm target: paris).Hardhat Ignition 🚀
Deploying [ AWToken ]
Batch #1 Executed AWToken#AWToken
[ AWToken ] successfully deployed 🚀
Deployed Addresses
AWToken#AWToken - 0x5FbDB2315678afecb367f032d93F642f64180aa3```
Now we can pass this address in tokensale contract constructor and just run npx hardhat ignition deploy ./ignition/modules/TokensaleDeployer.js --network
localhost
# Testing the contracts with Hardhat
Let's see how does the contracts work.
### AWToken.js test file
In test folder create AWToken.js and paste the following:
```jsconst { time, loadFixture,} = require("@nomicfoundation/hardhat-toolbox/network-helpers");const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");const { expect } = require("chai");
let token, owner, user1, tokenSale, tokenSaleAddress;
beforeEach(async function () { // Deploy AWToken contract const Token = await ethers.getContractFactory("AWToken"); token = await Token.deploy(); // console.log("Token address:", await token.getAddress()); const tokenAddress = await token.getAddress();
// Deploy TokenSale contract const TokenSale = await ethers.getContractFactory("TokenSale"); tokenSale = await TokenSale.deploy(tokenAddress, 5); console.log("tokenSale address:", await tokenSale.getAddress()); tokenSaleAddress = await tokenSale.getAddress();
// Get signers [owner, user1] = await ethers.getSigners();});
it("Has correct name and symbol", async function () { expect(await token.name()).to.equal("AWToken"); expect(await token.symbol()).to.equal("AWT");});
it("mints 100 tokens to the deployer", async function () { //but convert them to wei also const balance = await token.balanceOf(owner.address); expect(balance).to.equal(ethers.parseEther("100"));
//for the user1 now const balance1 = await token.balanceOf(user1.address); expect(balance1).to.equal(0);});```
### TokenSale.js test file
I'm not going to go over the testing. The code is here, feel free to study it.
```jsconst { time, loadFixture,} = require("@nomicfoundation/hardhat-toolbox/network-helpers");const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");const { expect } = require("chai");
let token, owner, tokenSale, tokenSaleAddress, user1, //NO whitelist user2, //whitelist user3, user4, //whitelist user5;
async function setupContracts() { // Deploy AWToken contract const Token = await ethers.getContractFactory("AWToken"); token = await Token.deploy(); // console.log("Token address:", await token.getAddress()); const tokenAddress = await token.getAddress();
// Deploy TokenSale contract const TokenSale = await ethers.getContractFactory("TokenSale"); tokenSale = await TokenSale.deploy( tokenAddress, ethers.parseUnits("5", "ether") ); console.log("tokenSale address:", await tokenSale.getAddress()); tokenSaleAddress = await tokenSale.getAddress();
// Get signers [owner, user1, user2, user3, user4, user5] = await ethers.getSigners();
//!owner sends all of the tokens to the tokenSale contract await token.transfer(tokenSaleAddress, 100); // * // ? // TODO}
beforeEach(async function () { //call the setup function await setupContracts();});
//utility functionsconst addToWhitelist = async function (user) { await tokenSale.connect(owner).addToWhitelist(user);};
it("whitelist sale is active as soon as the contract is deployed", async function () { expect(await tokenSale.isWhitelistSaleActive()).to.equal(true);});
it("standard sale is not active", async function () { expect(await tokenSale.isSaleActive()).to.equal(false);});
it("token price is 5 ETH", async function () { expect(await tokenSale.tokenPrice()).to.equal( ethers.parseUnits("5", "ether") );});
it("non whitelisted user cannot buy tokens", async function () { await expect( tokenSale.connect(user1).buyWhitesaleTokens(1) ).to.be.revertedWith("You are not whitelisted");});
it("owner can whitelist users", async function () { await addToWhitelist(user2.address); expect(await tokenSale.isWhitelisted(user2.address)).to.equal(true);});
it("owner can whitelist multiple users", async function () { await addToWhitelist(user3.address); await addToWhitelist(user4.address); expect(await tokenSale.isWhitelisted(user3.address)).to.equal(true); expect(await tokenSale.isWhitelisted(user4.address)).to.equal(true);});
it("non owner cannot whitelist users", async function () { await expect(tokenSale.connect(user1).addToWhitelist(user3.address)).to.be .reverted;});
it("whitelisted users can buy tokens", async function () { await addToWhitelist(user2.address); await tokenSale .connect(user2) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") });
expect(await token.balanceOf(user2.address)).to.equal(1); expect(await tokenSale.getTokenSold()).to.equal(1); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("5", "ether") );});
it("multiple whitelisted users can buy tokens", async function () { await addToWhitelist(user3.address); await addToWhitelist(user4.address); await tokenSale .connect(user3) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") }); await tokenSale .connect(user4) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") });
expect(await token.balanceOf(user3.address)).to.equal(1); expect(await token.balanceOf(user4.address)).to.equal(1); expect(await tokenSale.getTokenSold()).to.equal(2); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("10", "ether") );});
it("a whitelisted user can make multiple transactions until they reach the max sale limit", async function () { await addToWhitelist(user2.address); await tokenSale .connect(user2) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") });
expect(await token.balanceOf(user2.address)).to.equal(1); expect(await tokenSale.getTokenSold()).to.equal(1); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("5", "ether") ); //wait for one block await ethers.provider.send("evm_mine", []); //buy 2 more tokens await tokenSale .connect(user2) .buyWhitesaleTokens(2, { value: ethers.parseUnits("10", "ether") }); expect(await token.balanceOf(user2.address)).to.equal(3); expect(await tokenSale.getTokenSold()).to.equal(3); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("15", "ether") );});
it("owner pauses whitelist sale and whitelisted user cannot buy token", async function () { await addToWhitelist(user2.address);
await tokenSale.connect(owner).toggleWhitelistSaleActive(); expect(await tokenSale.isWhitelistSaleActive()).to.equal(false); //this should fail and revert //! It is very important that await comes before the expect here await expect( tokenSale .connect(user2) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") }) ).to.revertedWith("WhiteSale is not active");});
it("whitelisted user cannot buy more than max tokens", async function () { await addToWhitelist(user5.address); await expect( tokenSale .connect(user5) .buyWhitesaleTokens(5, { value: ethers.parseUnits("25", "ether") }) ).to.be.revertedWith("Amount must be between 1 and 3");});
it("whitelisted user buys 1 token first, then tries to exceed the mount and fails", async function () { await addToWhitelist(user5.address); await tokenSale .connect(user5) .buyWhitesaleTokens(1, { value: ethers.parseUnits("5", "ether") }); await expect( tokenSale .connect(user5) .buyWhitesaleTokens(3, { value: ethers.parseUnits("15", "ether") }) ).to.be.revertedWith("You cannot buy more than the specified max tokens");});
it("owner ends whitesale and everyone can buy tokens", async function () { await tokenSale.connect(owner).toggleWhitelistSaleActive(); await tokenSale.connect(owner).toggleSaleActive();
//wait for 1 block await ethers.provider.send("evm_mine", []);
await tokenSale .connect(user5) .buyTokens(2, { value: ethers.parseUnits("10", "ether") });
expect(await token.balanceOf(user5.address)).to.equal(2); expect(await tokenSale.getTokenSold()).to.equal(2); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("10", "ether") );});
it("a previously whitelisted user can buy 2 tokens in whitesale, and then buy the remaining 1 token in standard sale", async function () { await addToWhitelist(user5.address); await tokenSale .connect(user5) .buyWhitesaleTokens(2, { value: ethers.parseUnits("10", "ether") });
await tokenSale.connect(owner).toggleWhitelistSaleActive(); await tokenSale.connect(owner).toggleSaleActive();
//wait for 1 block await ethers.provider.send("evm_mine", []);
await tokenSale .connect(user5) .buyTokens(1, { value: ethers.parseUnits("5", "ether") }); expect(await token.balanceOf(user5.address)).to.equal(3); expect(await tokenSale.getTokenSold()).to.equal(3); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("15", "ether") );});
it("the same user cannot exceed the max amount by trying to buy 2 tokens in whitesale and 2 in standard sale", async function () { await addToWhitelist(user5.address); await tokenSale .connect(user5) .buyWhitesaleTokens(2, { value: ethers.parseUnits("10", "ether") });
await tokenSale.connect(owner).toggleWhitelistSaleActive(); await tokenSale.connect(owner).toggleSaleActive();
//wait for 1 block await ethers.provider.send("evm_mine", []);
await expect( tokenSale .connect(user5) .buyTokens(2, { value: ethers.parseUnits("10", "ether") }) ).to.be.revertedWith("You cannot buy more than the specified max tokens");});
it("owner can withdraw ETH", async function () { await tokenSale.connect(owner).toggleWhitelistSaleActive(); await tokenSale.connect(owner).toggleSaleActive(); //wait for 1 block await ethers.provider.send("evm_mine", []); await tokenSale .connect(user5) .buyTokens(2, { value: ethers.parseUnits("10", "ether") }); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("10", "ether") ); console.log("balance:", await ethers.provider.getBalance(tokenSaleAddress)); const ownerBalance = await ethers.provider.getBalance(owner.address); console.log("Owner balance:", ownerBalance); const gasLimit = 1000000; await tokenSale.connect(owner).withdrawEth({ gasLimit }); console.log("balance:", await ethers.provider.getBalance(tokenSaleAddress)); const newOwnerBalance = await ethers.provider.getBalance(owner.address); console.log("New Owner balance:", newOwnerBalance); expect(await ethers.provider.getBalance(tokenSaleAddress)).to.equal(0);
expect(newOwnerBalance).to.be.greaterThan(ownerBalance);});
it("non-owner cannot withdraw ETH", async function () { await tokenSale.connect(owner).toggleWhitelistSaleActive(); await tokenSale.connect(owner).toggleSaleActive(); //wait for 1 block await ethers.provider.send("evm_mine", []); await tokenSale .connect(user5) .buyTokens(2, { value: ethers.parseUnits("10", "ether") }); expect(await tokenSale.getBalance()).to.equal( ethers.parseUnits("10", "ether") ); await expect(tokenSale.connect(user5).withdrawEth()).to.be.reverted;});```
That's it for the backend. Now let's move on to the frontend.
# Building the frontend with React and connecting smart contracts
Create a new React project with Vite and install the packages that are shown in this package.json file
```JSON{ "name": "token-sale-frontend", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@tanstack/react-query": "^5.36.1", "@web3modal/wagmi": "^4.2.0", "localforage": "^1.10.0", "match-sorter": "^6.3.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.23.1", "sort-by": "^1.2.0", "viem": "^2.10.5", "wagmi": "^2.8.7" }, "devDependencies": { "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "vite": "^5.2.0" }}```
You see we're using wagmi, and react router for the admin section.
We need to ABIs of the smart contracts we deployed. They are found under ignition/ deployments / artifacts . For instance the one for AWToken contract is named on my machine as AWToken#AWToken.json
Open that file, copy the abi array and save it somewhere. Do the same for the token sale contract. Starts like this, you only need to copy the abi array.
```json{ "_format": "hh-sol-artifact-1", "contractName": "AWToken", "sourceName": "contracts/AWToken.sol", //you need to start here "abi": [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" },
... ... ...```
Now, in your frontend, create src/ABI and in them two folders awtokenAbi.js and tokensaleAbi.js and paste the abi with the address and so on with the following format:
```jsexport const awtokenAddress = "0xd25D204F1898DC97333409703b391Af8494105D7";export const awtokenAbi = [ { inputs: [], stateMutability: "nonpayable", type: "constructor", }, { inputs: [ { internalType: "address", name:
----------------```
From here, I'll just share the components that you need to put to get the app together.
### main.jsx
```jsimport React from "react";import ReactDOM from "react-dom/client";import App from "./App.jsx";import { WagmiProvider } from "wagmi";import { config } from "../config.js";import { createBrowserRouter, RouterProvider } from "react-router-dom";import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
import "./style/index.css";import Owner from "./routes/Owner.jsx";
const router = createBrowserRouter([ { path: "/owner", element: <Owner />, }, { path: "/", element: <App />, },]);
ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <WagmiProvider config={config}> <RouterProvider router={router}> <div style={{ height: "100vh", width: "100vw", }} > <App /> </div> </RouterProvider> </WagmiProvider> </QueryClientProvider> </React.StrictMode>);```
### App.jsx
````jsimport BuyTokens from "./components/BuyTokens.jsx";import ConnectWallet from "./components/ConnectWallet.jsx";import { useReadContract, useAccount, useBalance } from "wagmi";const App = () => { const { address, status, isConnected } = useAccount(); console.log(status);
return ( <div style={{ display: "flex", flexDirection: "column", alignItems: "center", }} > {" "} <h1>Awesome Token Sale</h1> {isConnected && <BuyTokens />} {isConnected == false && <ConnectWallet />}{" "} </div> );};
export default App;```
````
### src/routes/Owner.jsx
```jsimport { useState } from "react";import { useWriteContract, useDisconnect, useReadContract, useAccount,} from "wagmi";import { parseEther, parseUnits } from "viem";import { awtokenAbi, awtokenAddress } from "../ABI/awtokenAbi.js";import { tokensaleAddress, tokensaleAbi } from "../ABI/tokensaleAbi.js";import { Navigate } from "react-router-dom";
const Owner = () => { const { writeContract, status } = useWriteContract(); const { address, isConnected } = useAccount(); const [whitelistAddress, setWhitelistAddress] = useState(""); const [tokenState, setTokenState] = useState(5); // console.log(address); // if (address !== "0x1AF34385343fdf673aedB90A26ee64Bb01e1667D") { // return <Navigate to="/" replace />; // }
const toggleWhitelistSaleActive = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "toggleWhitelistSaleActive", }); } catch (error) { console.log(error); } }; const toggleSaleActive = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "toggleSaleActive", }); } catch (error) { console.log(error); } }; const togglePause = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "togglePause", }); } catch (error) { console.log(error); } }; const withdrawToken = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "withdrawToken", }); } catch (error) { console.log(error); } }; const withdrawEth = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "withdrawEth", }); } catch (error) { console.log(error); } }; const addToWhitelist = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "addToWhitelist", args: [whitelistAddress], }); } catch (error) { console.log(error); } }; const removeFromWhitelist = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "removeFromWhitelist", args: [whitelistAddress], }); } catch (error) { console.log(error); } }; const setTokenPrice = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "setTokenPrice", args: [tokenState], }); } catch (error) { console.log(error); } };
// const pauseStatus = useReadContract({ // abi: tokensaleAbi, // address: tokensaleAddress, // functionName: "getPaused", // });
return ( <div style={{ margin: "15px", }} > {address === "0x1AF34385343fdf673aedB90A26ee64Bb01e1667D" ? ( <div style={{ display: "flex", flexDirection: "column", alignItems: "start", justifyContent: "center", }} > <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => toggleWhitelistSaleActive()} > toggleWhitelistSaleActive </button> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => toggleSaleActive()} > toggleSaleActive </button> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => togglePause()} > togglePause </button> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => withdrawEth()} > withdrawEth </button> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => withdrawToken()} > withdrawToken </button> <div style={{ display: "flex", minWidth: "200px", maxWidth: "200px", }} > <input style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", boxSizing: "border-box", }} type="text" placeholder="address" onChange={(e) => setWhitelistAddress(e.target.value)} /> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => addToWhitelist()} > addToWhitelist </button> </div> <div style={{ display: "flex", minWidth: "200px", maxWidth: "200px", }} > <input style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", boxSizing: "border-box", }} type="text" placeholder="address" onChange={(e) => setWhitelistAddress(e.target.value)} /> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => removeFromWhitelist()} > removeFromWhitelist </button> </div>{" "} <div style={{ display: "flex", minWidth: "200px", maxWidth: "200px", }} > <input style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", boxSizing: "border-box", }} type="number" placeholder="tokenPrice" onChange={(e) => setTokenState(e.target.value)} /> <button style={{ minWidth: "200px", maxWidth: "200px", textAlign: "start", margin: "5px 5px", padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => setTokenPrice()} > setTokenPrice </button> </div> </div> ) : ( <div>Only owner can see the terminal</div> )} </div> );};
export default Owner;```
### src/components/BuyTokens.jsx
```jsimport { useState } from "react";import { useWriteContract, useDisconnect, useReadContract } from "wagmi";import { parseEther, parseUnits } from "viem";import { awtokenAbi, awtokenAddress } from "../ABI/awtokenAbi.js";import { tokensaleAddress, tokensaleAbi } from "../ABI/tokensaleAbi.js";const BuyTokens = () => { const [amount, setAmount] = useState(0); const { writeContract, failureReason } = useWriteContract(); const { disconnect } = useDisconnect(); console.log(amount);
const tokensSold = useReadContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "getTokenSold", });
async function calculateTokensLeft() { try { return 100 - tokensSold?.data; } catch (error) { console.log(error); } }
const whitesaleStatus = useReadContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "isWhitelistSaleActive", }); console.log(whitesaleStatus.data);
const saleStatus = useReadContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "isSaleActive", });
const tokenPrice = useReadContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "getTokenPrice", }); // console.log((tokenPrice?.data).toString());
const pauseStatus = useReadContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "getPaused", });
const callBuyWhitesaleTokens = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "buyWhitesaleTokens", args: [parseUnits(amount, 18)], }); } catch (error) { console.log(error); } };
const callBuyTokens = async () => { try { writeContract({ abi: tokensaleAbi, address: tokensaleAddress, functionName: "buyTokens", args: [parseUnits(amount, 18)], }); } catch (error) { console.log(error); } };
return ( <div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", alignItems: "center", }} > {tokenPrice?.data !== undefined && ( <h2>Buy Tokens for {(tokenPrice?.data).toString()} ETH</h2> )} {tokensSold?.data !== undefined && ( <h3>Tokens Sold: {(tokensSold?.data).toString()} / 100 </h3> )} <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", }} > <input type="number" onChange={(e) => { setAmount(e.target.value); }} style={{ padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", }} /> <button style={{ padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#90caf9", color: "black", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} > Buy </button> <button style={{ padding: "10px", borderRadius: "5px", border: "1px solid #ccc", marginBottom: "10px", backgroundColor: "#d62f2f", color: "white", cursor: "pointer", fontWeight: "bold", fontSize: "15px", }} onClick={() => disconnect()} > Disconnect </button> {whitesaleStatus?.data ? ( <h2>Whitesale is active</h2> ) : ( <h2>Whitesale is inactive</h2> )} </div> </div> );};
export default BuyTokens;```
### src/components/ConnecWallet.jsx
```js// import { useState } from "react";import { useConnect, useDisconnect, useAccount } from "wagmi";import { getChainId } from "@wagmi/core";import { injected } from "wagmi/connectors";//root config.js for chainsimport { config } from "../../config.js";const WalletComponent = () => { //hooks const { connect, status } = useConnect(); console.log(status); const { disconnect } = useDisconnect(); const account = useAccount(); console.log(account.isConnected); const chainId = getChainId(config); console.log(chainId);
const connectWallet = () => { connect({ connector: injected() }); };
//logic for address abbreviation
// const isSmallScreen = useMediaQuery("(max-width:600px)");
// State to track whether the full address is being displayed // const [showFullAddress, setShowFullAddress] = useState(false);
// Function to toggle the display of the full address on hover // const toggleAddressDisplay = () => { // setShowFullAddress(!showFullAddress); // };
// Abbreviate the address to display const abbreviatedAddress = ${account?.address?.substring( 0, 6 )}...${account?.address?.substring(account?.address?.length - 4)}
;
return ( <div> {status === "success" || account.isConnected ? ( <div> <div style={{ margin: "10px", fontSize: "15px", }} > Connected to {account?.address} </div> <button style={{ marginBottom: "10px", backgroundColor: "rgba(104, 110, 255, 1)", // Match background gradient's first color color: "white", // White text color for contrast fontWeight: "bold", // Bold text for emphasis padding: "10px 20px", // Adjust padding for desired button size borderRadius: 5, // Add some rounded corners border: "none", // Remove default border cursor: "pointer", // Set cursor to pointer on hover "&:hover": { // Style on hover for visual feedback backgroundColor: "rgba(141, 198, 255, 1)", // Match background gradient's second color for a subtle shift }, }} onClick={() => disconnect()} > Disconnect </button>{" "} </div> ) : ( <button style={{ backgroundColor: "rgba(104, 110, 255, 1)", // Match background gradient's first color color: "white", // White text color for contrast fontWeight: "bold", // Bold text for emphasis padding: "10px 20px", // Adjust padding for desired button size borderRadius: 5, // Add some rounded corners border: "none", // Remove default border cursor: "pointer", // Set cursor to pointer on hover "&:hover": { // Style on hover for visual feedback backgroundColor: "rgba(141, 198, 255, 1)", // Match background gradient's second color for a subtle shift }, }} onClick={connectWallet} > Connect </button> )} </div> );};
export default WalletComponent;```
That's it. If you've put all the pieces together, you should have a fully functional token sale dApp. Hope you enjoyed.
Subscribe to my newsletter
Read articles from Murat Can Yüksel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Murat Can Yüksel
Murat Can Yüksel
I build front end applications for EVM based blockchain protocols. You can contact me on my LinkedIn profile for business inquiries => https://www.linkedin.com/in/murat-can-y%C3%BCksel-2b1347119/