How to Build a Full‑Stack Decentralized Application with Hardhat, React, and WalletConnect
You’ve probably heard the buzz: “Web3 is the next big thing.” The hype is real, but most developers still wonder how to stitch together a smart contract, a front‑end, and a wallet connection without pulling their hair out. In this post I’ll walk you through a complete, working dApp from zero to launch, using tools that are free, well‑documented, and beginner‑friendly. By the end you’ll have a simple token‑minting app that runs on a local Hardhat network, talks to a React UI, and lets users sign transactions with WalletConnect. Let’s get our hands dirty.
Why Hardhat, React, and WalletConnect?
Hardhat is the Swiss‑army knife of Ethereum development. It gives you a local blockchain, a testing framework, and a smooth deployment pipeline. React is the de‑facto front‑end library for modern web apps, and it plays nicely with the ethers.js library that we’ll use to talk to the blockchain. WalletConnect is a bridge that lets mobile wallets (MetaMask Mobile, Trust Wallet, etc.) connect to a web page without any browser extensions. Together they form a full‑stack stack that mirrors what you’ll see in production, but without the complexity of a cloud provider.
Step 1 – Set Up the Hardhat Project
Open a terminal and run:
mkdir token-dapp && cd token-dapp
npm init -y
npm install --save-dev hardhat
npx hardhat
When Hardhat asks you to choose a template, pick “Create a basic sample project.” It will scaffold a contracts folder, a test folder, and a hardhat.config.js file. Accept the default Solidity version (0.8.17 at the time of writing).
Add ethers and dotenv
We’ll need ethers to interact with contracts and dotenv to keep private keys out of source code:
npm install --save ethers dotenv
Create a .env file (don’t forget to add it to .gitignore) and put a dummy private key for later deployment:
PRIVATE_KEY=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Step 2 – Write a Simple ERC‑20 Token
In contracts/Token.sol replace the sample contract with:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
We’re using OpenZeppelin’s ERC‑20 implementation because it’s battle‑tested. The constructor gives the deployer 1,000 tokens, and we expose a mint function for the UI to call.
Install OpenZeppelin contracts:
npm install @openzeppelin/contracts
Step 3 – Test the Contract
Create test/Token.test.js:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
it("Should assign the initial supply to the deployer", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy();
await token.deployed();
const balance = await token.balanceOf(owner.address);
expect(balance).to.equal(ethers.utils.parseUnits("1000", 18));
});
});
Run the test with:
npx hardhat test
If everything passes, you’ve got a solid contract foundation.
Step 4 – Deploy to a Local Hardhat Network
Add a deployment script scripts/deploy.js:
require("dotenv").config();
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with", deployer.address);
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy();
await token.deployed();
console.log("Token address:", token.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
Start a local node in one terminal:
npx hardhat node
In another terminal run the deploy script:
npx hardhat run scripts/deploy.js --network localhost
Copy the printed token address – you’ll need it for the front‑end.
Step 5 – Bootstrap the React Front‑End
Back in the project root:
npx create-react-app client
cd client
npm install ethers @walletconnect/web3-provider
Open client/src/App.js and replace the content with the following simplified UI:
import React, { useState, useEffect } from "react";
import { ethers } from "ethers";
import WalletConnectProvider from "@walletconnect/web3-provider";
const tokenAddress = "YOUR_TOKEN_ADDRESS_HERE";
const tokenAbi = [
"function balanceOf(address) view returns (uint256)",
"function mint(address to, uint256 amount) external"
];
function App() {
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [account, setAccount] = useState("");
const [balance, setBalance] = useState("0");
const [mintAmount, setMintAmount] = useState("");
// Connect via WalletConnect
const connectWallet = async () => {
const wcProvider = new WalletConnectProvider({
rpc: { 1337: "http://127.0.0.1:8545" } // Hardhat local chain id
});
await wcProvider.enable();
const web3Provider = new ethers.providers.Web3Provider(wcProvider);
const signer = web3Provider.getSigner();
const address = await signer.getAddress();
setProvider(web3Provider);
setSigner(signer);
setAccount(address);
};
// Load token balance
const loadBalance = async () => {
if (!signer) return;
const token = new ethers.Contract(tokenAddress, tokenAbi, signer);
const bal = await token.balanceOf(account);
setBalance(ethers.utils.formatUnits(bal, 18));
};
// Mint new tokens
const mintTokens = async () => {
if (!signer) return;
const token = new ethers.Contract(tokenAddress, tokenAbi, signer);
const amount = ethers.utils.parseUnits(mintAmount, 18);
const tx = await token.mint(account, amount);
await tx.wait();
loadBalance();
};
useEffect(() => {
if (account) loadBalance();
}, [account]);
return (
<div style={{ padding: "2rem" }}>
<h1>ChainCraft Token Mint</h1>
{account ? (
<>
<p>Connected as: {account}</p>
<p>Balance: {balance} MTK</p>
<input
type="text"
placeholder="Amount to mint"
value={mintAmount}
onChange={e => setMintAmount(e.target.value)}
/>
<button onClick={mintTokens}>Mint</button>
</>
) : (
<button onClick={connectWallet}>Connect Wallet (WalletConnect)</button>
)}
</div>
);
}
export default App;
Replace YOUR_TOKEN_ADDRESS_HERE with the address you copied earlier. The UI does three things:
- Opens a WalletConnect QR code that your phone wallet can scan.
- Shows the connected address and current token balance.
- Lets the user type an amount and mint new tokens to themselves.
Step 6 – Run the Full Stack
First, make sure the Hardhat node is still running (npx hardhat node). Then, from the client folder:
npm start
Your browser will open http://localhost:3000. Click “Connect Wallet,” scan the QR code with MetaMask Mobile (or any WalletConnect‑compatible app), and you should see your address appear. Hit “Mint,” type 10, and watch the balance jump.
A Quick Debug Tale
When I first tried this, the QR code kept looping back to “Connecting…”. Turns out I had the wrong RPC URL for the local chain. WalletConnect defaults to mainnet unless you explicitly tell it where to talk. Adding the rpc entry with 1337 (Hardhat’s default chain id) fixed it in seconds. A tiny detail, but it saved me an hour of head‑scratching.
Step 7 – Deploy to a Testnet (Optional)
If you want to go beyond localhost, create an Alchemy or Infura project, grab the endpoint for Sepolia, and add it to hardhat.config.js:
require("@nomiclabs/hardhat-ethers");
module.exports = {
solidity: "0.8.17",
networks: {
sepolia: {
url: "https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY",
accounts: [process.env.PRIVATE_KEY]
}
}
};
Run npx hardhat run scripts/deploy.js --network sepolia and update the React tokenAddress with the new address. In the WalletConnect config, replace the RPC entry with the Sepolia endpoint. That’s it – your dApp is now reachable by anyone on the testnet.
Wrap‑Up Thoughts
Building a full‑stack dApp feels a lot like assembling LEGO bricks. Hardhat gives you the sturdy base, React provides the colorful façade, and WalletConnect is the flexible connector that lets anyone with a phone join the game. The key is to keep each layer small, test often, and remember that the blockchain is still a young platform – bugs happen, and the community is quick to help.
At ChainCraft we love sharing these step‑by‑step guides because the best way to learn is by doing. Grab a coffee, fire up a terminal, and give this token‑minting app a spin. You’ll be surprised how fast you can go from “I read about Web3” to “I built something that actually works on a blockchain.”
- → How to Build an Interactive Digital Art Portfolio with React and Framer Motion @pixelatedpalette
- → Your First Open-Source Contribution: Fork, Fix, and Submit a Pull Request to a Popular React Library @codecraft
- → How to Add a Custom ChatGPT Widget to Your React Site @techtrails
- → From Idea to Launch: Building a Scalable Full-Stack App with React and Django @codecraftchronicles
- → From Bitcoin to Altcoins: A Comparative Look at Growth Potential @cryptochronicle