Building an Ethereum dApp with Next.js, Wagmi, and MetaMask


Over the last few years, decentralized applications (dApps) have become one of the most exciting areas in Web3. Instead of relying on centralized servers, dApps interact directly with blockchains—allowing users to own their assets, sign transactions, and engage with trustless systems.
In this tutorial, we’ll walk through building a simple Ethereum dApp using Next.js, Wagmi, and MetaMask. Our application will let users:
Connect their MetaMask wallet
Send ETH to another address on a testnet (Sepolia)
View their recent transactions in a paginated format
This project is designed as a starting point for anyone who wants to dive into Ethereum development and understand how wallet connections, transactions, and blockchain interactions work in practice.
We’ll use MetaMask as the crypto wallet for connecting and signing transactions, Wagmi (a React hooks library) for interacting with Ethereum, and Next.js for building the frontend of our dApp.
By the end of this guide, you’ll have a fully functional Ethereum dApp running on a testnet—ready to extend into something bigger, like token transfers or DeFi features.
Prerequisites
Before we dive into building the dApp, here’s what you’ll need to get started:
1. Basic Knowledge
Familiarity with React and Next.js fundamentals
A basic understanding of Ethereum and how transactions work
Some exposure to JavaScript/TypeScript
2. Installed Tools
Node.js & npm/yarn – To run the Next.js project
MetaMask – Installed as a browser extension or installed on mobile (for connecting and approving transactions)
VS Code (or any IDE) – To write and manage your project code
3. Ethereum Test Network Setup
We’ll use the Sepolia testnet for this project. That way, you don’t spend real ETH while testing.
👉 Steps:
Install MetaMask if you haven’t already.
Switch your MetaMask network to Sepolia Test Network.
Get some free test ETH from a Sepolia Faucet (just Google Sepolia faucet and request test ETH).
💡 Note: If you’re having trouble getting test ETH, you can also reach out to me via [email], and I can transfer some Sepolia ETH to your wallet for testing.
4. Libraries We’ll Use
Next.js – React framework for building the frontend
Wagmi – React hooks for Ethereum (easy wallet connection + transaction handling)
Viem – A low-level Ethereum library used under the hood by Wagmi
Tailwind CSS – For styling (optional, but makes UI much easier)
Once you have these prerequisites in place, you’ll be ready to start coding your dApp 🚀
🚀 Project Setup
Let’s set up our Ethereum dApp from scratch. We’ll be using Next.js for the frontend, Wagmi for wallet interactions, and MetaMask as our crypto wallet provider.
1. Create a Next.js App
First, create a new Next.js project using the official CLI:
npx create-next-app eth-dapp
cd eth-dapp
2. Install Dependencies
We’ll need Wagmi, viem, and ethers to connect with Ethereum.
npm install wagmi viem ethers
If you want pretty logging (optional), install pino-pretty:
npm install pino-pretty
3. Configure Wagmi
Inside your project, set up the Wagmi client in a Web3Provider.tsx
file. This will allow us to connect to Ethereum testnets like Sepolia.
"use client"
import {createConfig, http, WagmiProvider} from 'wagmi'
import {mainnet, sepolia} from 'wagmi/chains'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {metaMask} from "@wagmi/connectors";
import {injected} from "wagmi/connectors";
const queryClient = new QueryClient();
export const config = createConfig({
chains: [mainnet, sepolia],
connectors: [
metaMask(),
injected(),
],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
},
})
export function Web3Provider({children}: { children: React.ReactNode }) {
return <WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider></WagmiProvider>;
}
Now wrap your app in this provider (app/layout.tsx
in Next.js 13+):
import {Web3Provider} from "@/lib/web3/Web3Provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Web3Provider>{children}</Web3Provider>
</body>
</html>
);
}
4. Install MetaMask
Make sure you have the MetaMask extension installed in your browser or in your phone.
Switch to the Sepolia Testnet in MetaMask.
Request some test ETH from a Sepolia Faucet.
💡 If you’re unable to get test ETH, feel free to email me at [icon.gaurav806@gmail.com] and I can transfer some to your wallet for testing.
👉 That’s our base setup. Next, we’ll build the Wallet Connection component so users can connect their MetaMask wallet to the dApp.
🔗 Connecting Your Wallet
Now that our project is set up with Next.js and Wagmi, let’s add a Wallet Connect button so users can link their MetaMask wallet to the dApp.
1. Create a Wallet Connect Component
Inside components/WalletConnect.tsx
, add the following:
"use client"
import { useState } from "react"
import {useAccount, useConnect, useDisconnect} from "wagmi";
export default function WalletConnect({ onAddWallet }: {
onAddWallet: (address: string) => void
}) {
const {connectAsync, connectors, status , reset, error} = useConnect();
const { disconnect } = useDisconnect();
const handleConnect = async () => {
try {
disconnect(); // Disconnect any existing connection
const connector = connectors[0]; // Assuming the first connector is the one we want
const data = await connectAsync({ connector });
onAddWallet(data?.accounts?.[0]);
} catch (error) {
reset()
console.error("Failed to connect wallet:", error);
console.log(error)
}
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-2xl shadow-md">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Add Wallet</h2>
<div className="text-center">
<button
onClick={handleConnect}
className="w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
>
{status === 'pending'? 'Connecting...' :'Connect Using External Wallet'}
</button>
{error && <p className="text-red-600 mt-2">{error.message}</p>}
</div>
</div>
)
}
2. Add It to the Homepage
Open app/page.tsx
and include the WalletConnect component:
import WalletConnect from "@/components/WalletConnect";
export default function Home() {
return (
<div className="flex flex-col items-center justify-center px-6">
{/* ===== Hero Section with Wallet Connect ===== */}
<section className="text-center mt-20 max-w-3xl">
<h1 className="text-5xl font-extrabold text-gray-900 mb-6">
Manage Your Crypto with Ease 🚀
</h1>
<p className="text-lg text-gray-600 mb-8">
Create wallets, send & receive Ethereum, and track transactions —
all in one secure, easy-to-use app.
</p>
{/* Connect CTA */}
<div className="mt-6">
<WalletConnect onAddWallet={() => redirect('/dashboard')}/>
<p className="text-sm text-gray-500 mt-3">
Don’t have a wallet yet? Install{" "}
<Link
href="https://metamask.io/download/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
>
MetaMask
</Link>{" "}
to get started.
</p>
</div>
</section>
</div>
);
}
3. Test It
- Run your dev server:
npm run dev
Open
http://localhost:3000
.
Click Connect Wallet → MetaMask should pop up asking for approval.
Once connected, you’ll see get redirected to dashboard page.
✅ At this stage, your dApp can connect and disconnect a wallet.
Next, we’ll extend this by fetching account details and showing balances.
💰 Fetching Wallet Details & Balances
Once users connect their wallet, we can show them their Ethereum address, balance, and even a quick copy button. This helps them confirm they are on the right account before sending transactions.
1. Fetch Account Details with Wagmi
Wagmi provides hooks like useAccount
and useBalance
to make this easy. Inside app/dashboard/page.tsx
, add the following:
"use client";
import {useAccount, useBalance} from "wagmi";
import {useState} from "react";
import {Copy, Send, Wallet, ExternalLink, X} from "lucide-react";
import {redirect} from "next/navigation";
import SendTransaction from "@/components/SendTransaction";
export default function DashboardPage() {
const {address, isConnected} = useAccount();
const {data: balance} = useBalance({address});
const handleCopy = () => {
if (address) {
navigator.clipboard.writeText(address);
alert("Wallet address copied!");
}
};
if (!isConnected) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<div className="p-6 border rounded-lg shadow-md text-center w-full max-w-sm">
<p className="text-lg font-semibold">No wallet connected</p>
<p className="text-sm text-gray-500 mt-2">
Please connect your wallet to view your dashboard.
</p>
</div>
</div>
);
}
return (
<div className="p-4 sm:p-8 space-y-6 sm:space-y-8">
<h1 className="text-2xl sm:text-3xl font-bold">Dashboard</h1>
{/* Wallet Info */}
<div className="border rounded-lg shadow-md p-4 sm:p-6 flex items-center justify-between gap-4">
<div className="flex items-center gap-4 w-full">
<Wallet className="w-8 h-8 sm:w-10 sm:h-10 text-blue-600"/>
<div className="min-w-0 flex-1">
<p className="text-sm text-gray-500">Connected Wallet</p>
<p className="font-semibold text-sm sm:text-base break-all ">{address}</p>
</div>
</div>
<button
onClick={handleCopy}
className="p-2 border rounded-lg hover:bg-gray-100 flex-shrink-0"
>
<Copy className="w-4 h-4"/>
</button>
</div>
{/* Balance Info */}
<div className="border rounded-lg shadow-md p-4 sm:p-6">
<p className="text-sm text-gray-500">Total Balance</p>
<p className="text-xl sm:text-2xl font-bold mt-2">
{balance ? `${balance.formatted} ${balance.symbol}` : "Loading..."}
</p>
</div>
</div>
);
}
2. Test It
Connect your wallet.
Head to
/dashboard
.You should now see your wallet address, a copy icon, and your ETH balance.
✅ With this, your dApp shows real-time account info after connection.
Next up, we’ll implement the Send ETH feature with a modal for transfers.
🚀 Sending ETH with a Transaction Modal
One of the key features of our dApp is the ability to send ETH directly from the dashboard. For this, we’ll build a modal form where users can:
Enter a recipient’s address
Enter the amount to send
Confirm the transfer in MetaMask
We’ll use wagmi
’s useSendTransaction hook along with viem
’s parseEther
utility.
1. Send Transaction Component
Inside components/SendTransaction.tsx
, add the following:
"use client";
import { useState } from "react";
import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi";
import { parseEther } from "viem";
import Link from "next/link";
export default function SendTransaction() {
const [to, setTo] = useState<`0x${string}` | "">("");
const [amount, setAmount] = useState("");
const { data: txHash, isPending, sendTransaction, error } = useSendTransaction();
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({
hash: txHash, // hash of transaction
});
const handleSend = async () => {
try {
await sendTransaction({
to: to as `0x${string}`,
value: parseEther(amount), // convert ETH string to wei
});
} catch (err) {
console.error("Transaction failed:", err);
}
};
return (
<div className="p-4 max-w-md mx-auto space-y-4 rounded">
<h2 className="text-lg font-semibold">Send ETH</h2>
<input
type="text"
placeholder="Recipient address (0x...)"
value={to}
onChange={(e) => setTo(e.target.value as `0x${string}` | "")}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="Amount (ETH)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full p-2 border rounded"
/>
<button
onClick={handleSend}
disabled={isPending}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400"
>
{isPending ? "Sending..." : "Send"}
</button>
{/* Transaction States */}
{isPending && <p className="text-yellow-600">⏳ Transaction is being sent...</p>}
{isConfirming && <p className="text-blue-600">⏳ Waiting for confirmation...</p>}
{isConfirmed && (
<p className="text-green-600">
✅ Transaction confirmed!
</p>
)}
{/* Transaction Details */}
{txHash && (
<div className="mt-2 space-y-2">
<p className="text-sm break-all">
🔗 Tx Hash: {txHash}
</p>
<Link
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
className="underline text-blue-600"
>
View on Etherscan
</Link>
<br />
<Link
href={`/dashboard`}
className="underline text-purple-600"
>
Go to Dashboard
</Link>
</div>
)}
{error && (
<p className="text-sm text-red-600">
❌ Error: {error.message}
</p>
)}
</div>
);
}
2. Add Modal to Dashboard
We’ll trigger the modal using a Send Money button.
"use client";
import {useAccount, useBalance} from "wagmi";
import {useState} from "react";
import {Copy, Send, Wallet, ExternalLink, X} from "lucide-react";
import {redirect} from "next/navigation";
import SendTransaction from "@/components/SendTransaction";
export default function DashboardPage() {
const {address, isConnected} = useAccount();
const {data: balance} = useBalance({address});
const [isModalOpen, setIsModalOpen] = useState(false);
const handleCopy = () => {
if (address) {
navigator.clipboard.writeText(address);
alert("Wallet address copied!");
}
};
if (!isConnected) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<div className="p-6 border rounded-lg shadow-md text-center w-full max-w-sm">
<p className="text-lg font-semibold">No wallet connected</p>
<p className="text-sm text-gray-500 mt-2">
Please connect your wallet to view your dashboard.
</p>
</div>
</div>
);
}
return (
<div className="p-4 sm:p-8 space-y-6 sm:space-y-8">
<h1 className="text-2xl sm:text-3xl font-bold">Dashboard</h1>
{/* Wallet Info */}
<div className="border rounded-lg shadow-md p-4 sm:p-6 flex items-center justify-between gap-4">
<div className="flex items-center gap-4 w-full">
<Wallet className="w-8 h-8 sm:w-10 sm:h-10 text-blue-600"/>
<div className="min-w-0 flex-1">
<p className="text-sm text-gray-500">Connected Wallet</p>
<p className="font-semibold text-sm sm:text-base break-all ">{address}</p>
</div>
</div>
<button
onClick={handleCopy}
className="p-2 border rounded-lg hover:bg-gray-100 flex-shrink-0"
>
<Copy className="w-4 h-4"/>
</button>
</div>
{/* Balance Info */}
<div className="border rounded-lg shadow-md p-4 sm:p-6">
<p className="text-sm text-gray-500">Total Balance</p>
<p className="text-xl sm:text-2xl font-bold mt-2">
{balance ? `${balance.formatted} ${balance.symbol}` : "Loading..."}
</p>
</div>
{/* Actions */}
<div className="flex flex-col gap-4 sm:flex-row">
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 w-full sm:w-auto"
>
<Send className="w-4 h-4"/> Send Money
</button>
</div>
{/* Send Money Modal */}
{isModalOpen && (
<div className="fixed inset-0 flex items-center justify-center bg-black/40 p-4">
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6 relative">
<button
onClick={() => setIsModalOpen(false)}
className="absolute top-2 right-2 p-1 rounded hover:bg-gray-100"
>
<X className="w-5 h-5"/>
</button>
<SendTransaction/>
</div>
</div>
)}
</div>
);
}
3. Test the Flow
Connect your wallet
Enter a recipient address and amount
🎉 ETH is transferred on the testnet
Confirm the transaction in MetaMask or in Etherscan
✅ Now your dApp supports ETH transfers via a clean modal UI.
Next, we’ll build the Transactions Page to view past transfers with pagination.
📜 Viewing Past Transactions
Once users can send ETH, the next logical step is to view their past transactions. This helps them keep track of transfers, amounts, and recipients directly from our dApp.
We’ll use Etherscan APIs (or any testnet block explorer API) to fetch transaction history for the connected wallet. For simplicity, we’ll show a paginated table of transactions.
1. Transactions Page
Inside app/transactions.tsx
, add the following:
"use client";
import {useAccount} from "wagmi";
import {useEffect, useState} from "react";
import {ExternalLink} from "lucide-react";
interface Transaction {
hash: string;
from: string;
to: string;
value: string;
timeStamp: number;
}
export default function TransactionsPage() {
const {address, isConnected} = useAccount();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
// replace this with your provider API
const fetchTransactions = async (pageNumber: number) => {
if (!address) return;
setLoading(true);
try {
// Example using Etherscan API (you can use Alchemy/Moralis/Blockscout too)
const res = await fetch(
`https://api-sepolia.etherscan.io/api?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=${pageNumber}&offset=5&sort=desc&apikey=${process.env.NEXT_PUBLIC_ETHERSCAN_API_KEY}`
);
const data = await res.json();
setTransactions(data.result);
} catch (err) {
console.error("Error fetching transactions:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isConnected) {
fetchTransactions(page);
}
}, [page, isConnected]);
if (!isConnected) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="p-6 border rounded-lg shadow-md text-center">
<p className="text-lg font-semibold">No wallet connected</p>
<p className="text-sm text-gray-500 mt-2">
Connect your wallet to view transactions.
</p>
</div>
</div>
);
}
return (
<div className="p-8 w-full">
<h1 className="text-2xl font-bold mb-6">Transactions</h1>
{loading ? (
<p>Loading transactions...</p>
) : (
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full text-sm">
<thead className="bg-gray-100">
<tr>
<th className="px-4 py-2 text-left">Sender</th>
<th className="px-4 py-2 text-left">Receiver</th>
<th className="px-4 py-2">Type</th>
<th className="px-4 py-2">Date</th>
<th className="px-4 py-2">Amount (ETH)</th>
<th className="px-4 py-2">Status</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => {
const type =
tx.from.toLowerCase() === address?.toLowerCase()
? "Sent"
: "Received";
const date = new Date(
parseInt(tx.timeStamp.toString()) * 1000
).toLocaleString();
const amount = (parseFloat(tx.value) / 1e18).toFixed(5);
return (
<tr key={tx.hash} className="border-t hover:bg-gray-50">
<td className="px-4 py-2">{tx.from}</td>
<td className="px-4 py-2">{tx.to}</td>
<td className="px-4 py-2 text-center">{type}</td>
<td className="px-4 py-2">{date}</td>
<td className="px-4 py-2">{amount}</td>
<td className="px-4 py-2 text-center">
<a
href={`https://sepolia.etherscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-blue-600 hover:underline"
>
View <ExternalLink className="w-4 h-4"/>
</a>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* Pagination Controls */}
<div className="flex justify-between mt-4">
<button
disabled={page === 1}
onClick={() => setPage((p) => Math.max(p - 1, 1))}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
<span className="px-4 py-2">Page {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
className="px-4 py-2 border rounded"
>
Next
</button>
</div>
</div>
);
}
2. Key Features
Etherscan API integration – fetches real transaction data
Pagination support – only 5 transactions per page
Clickable transaction hashes – link to Sepolia Etherscan explorer
Formatted ETH values & timestamps
3. User Flow
User navigates to Transactions Page
dApp fetches their latest transactions from Sepolia testnet
Users can browse through pages for full history
Each transaction is clickable → Opens in Etherscan
In this guide, we built a simple yet powerful Ethereum dApp using Next.js, Wagmi, and MetaMask.
We walked through:
🔗 Connecting a crypto wallet with MetaMask
👛 Checking wallet balances on the Sepolia testnet
💸 Sending ETH transactions directly from the browser
📜 Viewing transaction history with pagination and Etherscan links
This project gives you a solid foundation for building decentralized applications. From here, you can expand in many exciting directions:
🚀 Possible Next Steps
Token Transfers (ERC-20): Add support for sending tokens like USDC or DAI.
Smart Contracts: Deploy your own contracts and interact with them.
NFT Support (ERC-721/1155): Mint, transfer, and display NFTs in your dApp.
Improved UI/UX: Add notifications, confirmations, and a polished dashboard.
Security Features: Integrate rate limiting, error handling, and input validation.
👉 This dApp is just the starting point. With these building blocks, you can craft anything from a DeFi dashboard to a full-scale Web3 application.
🌐 Try it out yourself
You can explore the live version here: Deployed dApp
💻 View or fork the code
Check out the full source code on GitHub: GitHub Repo
🔮 What’s Next in Our Blog Series?
In the next post, we’ll explore integrating smart contracts into our dApp so that users can interact with decentralized logic directly from the UI.
Subscribe to my newsletter
Read articles from Gaurav Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Gaurav Kumar
Gaurav Kumar
Hey! I'm Gaurav, a tech writer who's all about MERN, Next.js, and GraphQL. I love breaking down complex concepts into easy-to-understand articles. I've got a solid grip on MongoDB, Express.js, React, Node.js, and Next.js to supercharge app performance and user experiences. And let me tell you, GraphQL is a game-changer for data retrieval and manipulation. I've had the opportunity to share my knowledge through tutorials and best practices in top tech publications. My mission? Empowering developers and building a supportive community. Let's level up together with my insightful articles that'll help you create top-notch applications!