Interface
Prerequisites
In blockchain projects, it's often not practical to directly use ethers.js or web3.js to interact with MetaMask, as it can be cumbersome. To address this and facilitate faster connection and interaction with the blockchain, many excellent third-party libraries have been developed, such as web3-react, web3-modal, rainbow-kit (which is an encapsulation based on wagmi), and wagmi. To quickly build the Coin Pusher dapp, we will use the Next.js, Wagmi, Rainbow-kit, and Viem technology stack for development.
Nextjs: It is a React framework for building server-rendered and static websites and web applications.
Wagmi: It is a React Hooks library for Ethereum, designed to simplify interactions with the Ethereum blockchain and smart contracts. It supports over 40 out-of-the-box Ethereum features for accounts, wallets, contracts, transactions, signatures, ENS, and more. Wagmi also supports almost all wallets through its official connectors, EIP-6963 support, and extensible API.
Rainbow-kit: It is a React library that provides components to build a Connect Wallet UI with just a few lines of code. It supports many wallets, including Metamask, Rainbow, Coinbase Wallet, WalletConnect, and more. It is also highly customizable and comes with stunning built-in themes.
Viem: It is a low-level TypeScript interface for Ethereum that enables developers to interact with the Ethereum blockchain, including: JSON-RPC API abstraction, smart contract interactions, wallet and signature implementations, encoding/parsing utilities, and more. Wagmi Core is essentially a wrapper for Viem, providing multi-chain capabilities through Wagmi Config and automatic account management through connectors.
Final effect display


Create a Nextjs project
mkdir coin_pusher_app
cd coin_pusher_app
npx create-next-app ./

The default version of React used in the current Next.js project is version 19, but it is not very stable and may have compatibility issues with other dependencies. Therefore, I suggest changing the version of react
and react-dom
in package.json
to "^18.2.0"
, and then re-running npm install
.

Run the project in development mode.
npm run dev
The image below is the Next.js initialization page.

Project structure.

This is the project structure when initializing Next.js, but the contract
folder is newly created, where is store the contract's ABI and contract address. You can copy the ABI and address of the contract you just deployed.
Install dependencies
Install RainbowKit and its peer dependencies, wagmi, viem, and @tanstack/react-query.
npm install @rainbow-me/rainbowkit wagmi viem@2.x @tanstack/react-query react-hot-toast
Note: RainbowKit is a React library.
Import the RainbowKit configuration
Copy the following code snippet into the /context/Web3Provider.tsx
file.
"use client";
import "@rainbow-me/rainbowkit/styles.css";
import {
getDefaultConfig,
RainbowKitProvider,
lightTheme,
} from "@rainbow-me/rainbowkit";
import { WagmiProvider } from "wagmi";
import { sepolia } from "wagmi/chains";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const config = getDefaultConfig({
appName: "Coin Pusher",
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
chains: [sepolia],
ssr: true,
});
const queryClient = new QueryClient();
export default function Web3Provider({
children,
}: {
children: React.ReactNode;
}) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={lightTheme({
accentColor: "#FF652B",
accentColorForeground: "white",
})}
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
This is the configuration file for RainbowKit. I have separated it and placed it in /context/Web3Provider.tsx
to make the project structure clearer. Configure the required chains and generate the necessary connectors. You also need to set up a wagmi configuration.
Note: Every dApp that relies on WalletConnect now needs to obtain a
projectId
from WalletConnect Cloud. This is absolutely free and only takes a few minutes.
Place the project ID in the .env
folder.
NEXT_PUBLIC_PROJECT_ID=<YOUR_PROJECT_ID>
Import the Web3Provider
configuration file into app/layout.tsx
.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Web3Provider>{children}</Web3Provider>
<Toaster />
</body>
</html>
);
}
The Toaster
component comes from the react-hot-toast
library, which is a lightweight, customizable notification library designed for React applications.
Retrieve the status from contact
Copy the following code snippet into the hooks/useGameStatus.ts
file.
import { useEffect, useState } from "react";
import { useAccount, useReadContracts } from "wagmi";
import { abi } from "@/contract/contract-abi.json";
import { coinPusherAddress } from "@/contract/contract-address.json";
import { Address, formatUnits, zeroAddress } from "viem";
type GameStatus = {
isCompleted: boolean;
winner: Address;
sum: string;
target: string;
state: number;
myDeposit: string;
};
type GameStatusResult = {
isCompleted: boolean;
winner: Address;
sum: bigint;
target: bigint;
state: number;
};
const wagmigotchiContract = {
address: coinPusherAddress as `0x${string}`,
abi: abi,
} as const;
export const useGameStatus = () => {
const { address } = useAccount();
const [gameStatus, setGameStatus] = useState<GameStatus>({
isCompleted: false,
winner: zeroAddress,
sum: "0",
target: "0",
state: 0,
myDeposit: "0",
});
const result = useReadContracts({
contracts: [
{
...wagmigotchiContract,
functionName: "getGameStatus",
},
{
...wagmigotchiContract,
functionName: "depositOf",
args: [address],
},
],
});
const data = result.data as [
{ result: GameStatusResult },
{ result: bigint | undefined }
];
const { isLoading, refetch } = result;
useEffect(() => {
if (!data) return;
setGameStatus({
isCompleted: data[0].result.isCompleted,
winner: data[0].result.winner,
sum: formatUnits(data[0].result.sum, 6),
target: formatUnits(data[0].result.target, 6),
state: data[0].result.state,
myDeposit: formatUnits(data[1].result || BigInt(0), 6),
});
}, [data]);
return {
gameStatus,
isLoading,
refetch,
};
};
This is a custom Hook named useGameStatus
, which is mainly used to retrieve the status of the Coin Pusher game.
Explanation:
useAccount: a Hook for getting current account.
gameStatues: This is a state declaration in a React Hook, using the
useState
Hook, which stores the state of the game.useReadContracts:
useReadContracts
is a Hook provided by wagmi for querying multiple contract functions at once. We use it to query thegetGameStatus
anddepositOf
functions.wagmigotchiContract
contains the contract's ABI and address, which are necessary information for calling smart contracts.isLoading and refetch:
isLoading
andrefetch
are both data returned from theuseReadContracts
hook.isLoading
is a boolean value indicating whether the data is currently loading, andrefetch
is a function used to fetch the data again.useEffect: This is a React
useEffect
Hook, used to listen for and handle updates to game status data. When thedata
changes, the callback function withinuseEffect
is executed, and within this callback function, the data fromdata
is synchronized withgameStatus
.formatUnits:
formatUnits
is a utility function in viem (an Ethereum development library), primarily used to convert large on-chain values (usually represented in their smallest units) into a human-readable format.
Deposit function
Copy the following code snippet into the hooks/useGameDeposit.ts
file.
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { parseEther } from "viem";
import { coinPusherAddress } from "@/contract/contract-address.json";
import { abi } from "@/contract/contract-abi.json";
import toast from "react-hot-toast";
const wagmigotchiContract = {
address: coinPusherAddress as `0x${string}`,
abi: abi,
} as const;
const VALUE_BY_FLAG = ["0.001", "0.005", "0.01"];
export const useGameDeposit = () => {
const { data: hash, isPending, writeContractAsync } = useWriteContract();
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({
hash,
});
const handleDeposit = async (flag: number) => {
try {
const value = VALUE_BY_FLAG[flag];
await writeContractAsync({
...wagmigotchiContract,
functionName: "deposit",
args: [flag],
value: parseEther(value),
});
toast.loading("Confirming transaction...", { id: "hash" });
} catch (error) {
toast.error("Failed to send transaction");
console.error(error);
}
};
return {
handleDeposit,
isPending,
isConfirming,
isConfirmed,
};
};
This is a custom Hook named useGameDeposit
, which is used to handle the deposit functionality in blockchain games.
Explanation:
useWriteContract:
useWriteContract
is a Hook in Wagmi designed for executing write functions on a contract. It returns several properties, from which we extract attributes using destructuring:data
,isPending
, andwriteContractAsync
.data
is the transaction hash returned after sending a transaction.isPending
is a boolean value indicating whether there is an ongoing write operation (such as a transaction); ifisPending
returnstrue
, it means the transaction has not yet been confirmed by the blockchain.writeContractAsync
is an asynchronous function that allows you to send transactions to a smart contract.useWriteForTransactionReceipt:
useWaitForTransactionReceipt
is also a hook provided by Wagmi. The role of this hook is to listen for a transaction receipt, ensuring that the transaction has been fully confirmed.isLoading
is a property from the object returned byuseWaitForTransactionReceipt
, which is renamed toisConfirming
here. This property is a boolean value indicating whether the transaction is still pending confirmation.handleDeposit: This function accepts a flag parameter and retrieves the corresponding amount from the
VALUE_BY_FLAG
array. It then useswriteContractAsync
to call the smart contract'sdeposit
function, passing theflag
as an argument withargs: [flag]
. Thevalue: parseEther(value)
converts the ETH amount to Wei units for the transaction.
CSS Style
Copy the following code snippet into the app/globals.css
file.
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@layer components {
.dataPanel {
@apply relative m-4 flex h-80 w-11/12 p-8 flex-col items-center justify-between gap-4 rounded-[38px] border border-black bg-white sm:m-8 sm:h-96 sm:w-[450px] lg:w-[400px] xl:h-[400px] 2xl:mx-10 2xl:h-[450px] 2xl:w-[500px] 2xl:border-2;
}
.dataItem {
@apply flex justify-between w-full border-b border-black pb-2;
}
.depositButton {
@apply w-full rounded-lg bg-[#FF652B] border 2xl:border-2 border-black p-4 text-white;
}
}
Coin Pusher Interface
Copy the following code snippet into the app/page.tsx
file.
In Next.js, app/page.tsx
is a special file used to define the root page of the application, that is, the UI that is displayed when you visit the root URL of the application (usually /
).
"use client";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useGameStatus } from "@/hooks/useGameStatus";
import { zeroAddress } from "viem";
import { useGameDeposit } from "@/hooks/useGameDeposit";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
export const POOL_STATE = [
"Initial",
"Launching",
"Launched",
"Completed",
"Revealing",
"Revealed",
];
const DEPOSIT_AMOUNTS = [
{ value: "0.001", flag: 0 },
{ value: "0.005", flag: 1 },
{ value: "0.01", flag: 2 },
];
const Home = () => {
const [activeDepositFlag, setActiveDepositFlag] = useState<number | null>(
null
);
const { gameStatus, isLoading, refetch: refetchGameStatus } = useGameStatus();
const { handleDeposit, isPending, isConfirmed, isConfirming } =
useGameDeposit();
const isDepositing = isPending || isConfirming;
const handleDepositClick = (flag: number) => {
setActiveDepositFlag(flag);
handleDeposit(flag);
};
useEffect(() => {
if (isConfirmed) {
toast.dismiss("hash");
toast.success("Transaction confirmed!");
refetchGameStatus();
setActiveDepositFlag(null);
}
}, [isConfirmed, refetchGameStatus]);
if (isLoading) {
return;
}
return (
<div className="flex min-h-svh w-screen items-center bg-white">
<div className="mx-auto flex flex-col items-center gap-6 max-w-[1440px] px-4 md:px-10">
{/* "ConnectButton" component from RainbowKit */}
<ConnectButton />
<div className="flex flex-col items-center rounded-[38px] border border-black bg-[#FF652B] shadow lg:mb-0 2xl:border-2">
<div className="dataPanel">
<p className="2x:mb-10 rounded-full border border-[#FF652B] px-5 py-2 text-[#FF652B] text-lg">
pool state: {POOL_STATE[gameStatus.state]}
</p>
<div className="flex flex-col gap-4 w-full text-lg ">
<div className="dataItem">
<p>Pool Winner:</p>
<p>
{gameStatus.winner === zeroAddress
? "No Winner Yet"
: gameStatus.winner.slice(0, 6) +
"..." +
gameStatus.winner.slice(-4)}
</p>
</div>
<div className="dataItem">
<p>Pool Target:</p>
<p>{gameStatus.target} ETH</p>
</div>
<div className="dataItem">
<p>Pool Sum:</p>
<p>{gameStatus.sum} ETH</p>
</div>
<div className="dataItem">
<p>My Deposit:</p>
<p>{gameStatus.myDeposit} ETH</p>
</div>
</div>
</div>
<div className="w-full px-4 sm:px-10 flex gap-6 pb-10 2xl:pb-20">
{DEPOSIT_AMOUNTS.map(({ value, flag }) => (
<button
key={flag}
className={`depositButton ${
activeDepositFlag === flag ? "bg-white text-black" : ""
}`}
onClick={() => handleDepositClick(flag)}
disabled={isDepositing}
>
{activeDepositFlag === flag && isDepositing ? (
<span>Loading...</span>
) : (
`${value}ETH`
)}
</button>
))}
</div>
</div>
</div>
</div>
);
};
export default Home;
Run npm run dev
, and then you will be able to view the page locally on port http://localhost:3000
.
Explanation:
This code snippet imports the ConnectButton
component from RainbowKit, which is primarily responsible for rendering the connect/disconnect wallet button.Then render the gameStatus
data returned from the useGameStatus()
hook onto the interface.

const handleDepositClick = (flag: number) => {
setActiveDepositFlag(flag);
handleDeposit(flag);
};
The handleDepositClick
function receives a flag
parameter and primarily accomplishes two operations:
setActiveDepositFlag(flag)
is a state update function used to track the deposit button currently selected by the user.handleDeposit(flag)
is a function obtained from theuseGameDeposit
custom Hook, responsible for handling the actual deposit transaction logic.
useEffect(() => {
if (isConfirmed) {
toast.dismiss("hash");
toast.success("Transaction confirmed!");
refetchGameStatus();
setActiveDepositFlag(null);
}
}, [isConfirmed, refetchGameStatus]);
The purpose of this code is to refetch the game status using refetchGameStatus
and reset the current active deposit state when the transaction is confirmed (isConfirmed=true
).
Next, you can try playing the game.

Click the button corresponding to the amount you wish to deposit, and then the button you clicked will turn white and display 'Loading...'.

After a few seconds, the MetaMask wallet will pop up a transaction confirmation window. Click the confirm button to send a deposit transaction.

You can switch between multiple different wallet addresses to send deposit request until the total game amount reaches the encrypted target.
Note: In contract have limited each player to make only one deposit, and making multiple deposits will result in an error.
Congratulations, you have completed the development of the entire dapp.
Last updated