Skip to main content

Developing a dApp with WAGMI and RainbowKit

In this guide, we'll leverage Next.js, WAGMI, and RainbowKit to create a user-friendly dApp that interacts with the NFT smart contract we deployed on Soneium Minato earlier. With this setup, you'll enable users to connect their wallets, mint NFTs, and view their NFT balances directly from a sleek frontend.


Prerequisites

Ensure you have the following installed:

  • Node.js
  • pnpm (optional, but recommended)

Example Code Repository

You can find the example code we used for this tutorial here.

Step 1: Create a Hardhat Project

To kickstart your Next.js project, follow these steps:

  1. Initialize a Next.js project:

    $ npx create-next-app@latest nft-app

    ✔ Would you like to use TypeScript? … No / Yes
    ✔ Would you like to use ESLint? … No / Yes
    ✔ Would you like to use Tailwind CSS? … No / Yes
    ✔ Would you like to use `src/` directory? … No / Yes
    ✔ Would you like to use App Router? (recommended) … No / Yes
    ✔ Would you like to customize the default import alias (@/*)? … No / Yes
  2. Open the project directory: Navigate to the project directory and remove the package-lock.json file, as we will use pnpm for package management.

    $ cd ./nft-app/
    $ rm package-lock.json
  3. Install dependencies:

    $ pnpm i @rainbow-me/rainbowkit wagmi viem@2.x @tanstack/react-query
  4. Start the development server: Launch your Next.js app locally:

    $ pnpm dev

Step 2: Add Reown (WalletConnect) project ID

As every dApp that relies on WalletConnect, RainbowKit needs to obtain a projectId from Reown Cloud (previously known as WalletConnect Cloud). Sign up for free and get your ID:

.env
NEXT_PUBLIC_REOWN_PROJECT = "YOUR_PROJECT_ID"

Step 3: Create WAGMI's config file

Let’s configure WAGMI for seamless interaction with the blockchain:

  1. Create modules folder within src folder.
  2. Create wagmi folder inside the modules folder.
  3. Add a config.ts file within the wagmi folder.
  1. (Optional) Download the Soneium logo file symbol-full-color.svg from here.
  2. (Optional) Create public folder under the root folder and add the logo file.

Folder Structure

📁public
└── symbol-full-color.svg
📁src
└── 📁app
└── 📁modules
└── 📁wagmi
└── config.ts
src/modules/wagmi/config.ts
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { sepolia, soneiumMinato } from "viem/chains";

const reownProjectId = process.env.NEXT_PUBLIC_REOWN_PROJECT;

const minato = {
...soneiumMinato,
name: "Soneium Minato",
iconUrl: "/symbol-full-color.svg",
};

export const WAGMI_CONFIG = getDefaultConfig({
appName: "YOUR_DAPP_NAME",
projectId: reownProjectId as string,
chains: [minato, sepolia],
ssr: true,
});
tip

You can add more than one chain if your dApp supports multiple chains.

export const WAGMI_CONFIG = getDefaultConfig({
appName: "YOUR_DAPP_NAME",
projectId: reownProjectId as string,
chains: [minato, sepolia],
ssr: true,
});

Step 4: Create a Wallet Provider Wrapper

We’ll create a wrapper that integrates WAGMI, QueryClient, and RainbowKit providers.

  1. Create components folder under the wagmi folder.
  2. Add WalletProvider.tsx file inside the components folder.

Folder Structure

📁src
└── 📁app
└── 📁modules
└── 📁wagmi
└── 📁components
└── WalletProvider.tsx
src/modules/wagmi/components/WalletProvider.tsx
"use client";

import { WAGMI_CONFIG } from "@/modules/wagmi/config";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { soneiumMinato } from "viem/chains";
import { WagmiProvider } from "wagmi";

const client = new QueryClient();

export function WalletProvider({
children,
}: React.PropsWithChildren): JSX.Element {
return (
<WagmiProvider config={WAGMI_CONFIG}>
<QueryClientProvider client={client}>
<RainbowKitProvider initialChain={soneiumMinato}>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
tip

In RainbowKitProvider, you can set the initialChain to control which blockchain your dApp connects to first, making it easier for users to interact with the right network if your dApp supports multiple chains.

<RainbowKitProvider initialChain={soneiumMinato}>
{children}
</RainbowKitProvider>

Step 5: Add the Connect Button in the Header

We are going to create a header file with containing the ConnectButton component. RainbowKit will now handle your user's wallet selection, display wallet/transaction information and handle network/wallet switching.

We’ll create a header component featuring the ConnectButton from RainbowKit, making it easy for users to connect their wallets and handle network/wallet switching.

  1. Create common folder under modules folder
  2. Create components folder inside the common folder
  3. Add Header.tsx and Header.module.css files under the components folder

Folder Structure

📁src
└── 📁app
└── 📁modules
└── 📁common
└── 📁components
└── Header.module.css
└── Header.tsx
src/modules/common/components/Header.tsx
import { ConnectButton } from "@rainbow-me/rainbowkit";
import styles from "./Header.module.css";

export function Header(): JSX.Element {
return (
<header className={styles.wrapper}>
<ConnectButton />
</header>
);
}

With this setup, RainbowKit will manage the wallet selection UI, handle wallet connections.


Step 6: Update the Layout file

Edit layout.tsx to include RainbowKit’s styles and wrap the app with the WalletProvider:

src/app/layout.tsx
import type { Metadata } from "next";
import { Header } from "@/modules/common/components/Header";
import { WalletProvider } from "@/modules/wagmi/components/WalletProvider";
import "@rainbow-me/rainbowkit/styles.css";
import "./globals.css";

export const metadata: Metadata = {
title: "WAGMI + RainbowKit Demo App",
description:
"A Demo app for minting NFTs on Soneium Minato by using WAGMI and Rainbowkit.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<WalletProvider>
<Header />
{children}
</WalletProvider>
</body>
</html>
);
}

After completing this, your app will look like this:

Once a user connects their wallet, the connected chain and account information will appear in the header:

Step 7: Update the Top page and Add the ABI

Now that the app's structure is ready, let's build the main page where users can interact with the NFT smart contract. We'll include logic to mint NFTs, check balances, and display transaction details. Additionally, we'll add the contract's ABI to enable communication with the deployed smart contract.

Adding the ABI

The ABI acts as a bridge between our frontend and the smart contract, defining the methods we can call. Since our contract has been deployed and verified in the previous chapter, you can find the ABI in the Soneium Minato Explorer.

  1. Create an abi folder under the wagmi folder.
  2. Add the ABI to a file named DemoNFT.ts.

Creating the Main Page

Update the page.tsx file to manage user interactions, such as checking their NFT balance and minting new NFTs. Remember to update the nftContractAddress variable with your deployed contract's address.

Folder Structure

📁src
└── 📁app
└── page.module.css
└── page.tsx
└── 📁modules
└── 📁wagmi
└── 📁abi
└── DemoNFT.ts
src/app/page.tsx
"use client";

import { useEffect, useState } from "react";
import { type Address } from "viem";
import {
useAccount,
useBalance,
useChainId,
usePublicClient,
useReadContract,
useSwitchChain,
useWalletClient,
} from "wagmi";
import { soneiumMinato } from "viem/chains";
import NFT_ABI from "../modules/wagmi/abi/DemoNFT";
import styles from "./page.module.css";

// Todo: Update to the correct address for the deployed contract
const nftContractAddress = "0xFd0dA2fC3ac7D18D133b6A87379b80165bF04E14";
const faucetDocs = "https://docs.soneium.org/docs/builders/tools/faucets";

export default function Home(): JSX.Element {
const [txDetails, setTxDetails] = useState<string>("");
const { address: walletAddress } = useAccount();

const { switchChain } = useSwitchChain();
const connectedId = useChainId();
const chainId = soneiumMinato.id;
const isConnectedToMinato = connectedId === soneiumMinato.id;

const { data: walletClient } = useWalletClient({
chainId,
account: walletAddress,
});

const publicClient = usePublicClient({
chainId,
});

const [isPending, setIsPending] = useState(false);
const { data: bal } = useBalance({
address: walletAddress,
chainId,
});
const isBalanceZero = bal?.value.toString() === "0";

const { data, isFetched, refetch } = useReadContract({
abi: NFT_ABI,
address: nftContractAddress,
functionName: "balanceOf",
args: [walletAddress as Address],
});

async function mintNft(): Promise<void> {
if (!walletClient || !publicClient || !walletAddress) return;
try {
setIsPending(true);
setTxDetails("");
const tx = {
account: walletAddress as Address,
address: nftContractAddress as Address,
abi: NFT_ABI,
functionName: "safeMint",
args: [walletAddress],
} as const;
const { request } = await publicClient.simulateContract(tx);
const hash = await walletClient.writeContract(request);
await publicClient.waitForTransactionReceipt({
hash,
});
setTxDetails(`https://explorer-testnet.soneium.org/tx/${hash}`);
await refetch();
} catch (error) {
console.error(error);
} finally {
setIsPending(false);
}
}

function textNftBalances(bal: string): string {
const balance = Number(bal);
if (balance > 1) {
return `You have ${balance} NFTs`;
} else if (balance === 1) {
return `You have ${balance} NFT`;
} else {
return `You don't own any NFTs yet`;
}
}
useEffect(() => {
setTxDetails("");
}, [walletAddress]);

// Memo: display the page after fetching the NFT balance
return !isFetched ? (
<div />
) : (
<div className={styles.container}>
<div className={styles.rowBalance}>
{walletAddress && (
<span>{textNftBalances(data?.toString() || "0")}</span>
)}
</div>
<br />

<button
disabled={
isPending || !walletAddress || isBalanceZero || !isConnectedToMinato
}
className={styles.buttonAction}
onClick={mintNft}
type="button"
>
{isPending ? "Confirming..." : "Mint NFT"}
</button>

{txDetails && (
<div className={styles.txDetails}>
<span>🎉 Congrats! Your NFT has been minted 🐣 </span>
<a
href={txDetails}
target="_blank"
rel="noreferrer"
className={styles.txLink}
>
View transaction
</a>
</div>
)}

{walletAddress && isBalanceZero && (
<div className={styles.rowChecker}>
<span className={styles.textError}>
You don't have enough ETH balance to mint NFT
</span>
<a
href={faucetDocs}
target="_blank"
rel="noreferrer"
className={styles.txLink}
>
ETH Faucet
</a>
</div>
)}

{!isConnectedToMinato && walletAddress && (
<div className={styles.rowChecker}>
<span className={styles.textError}>
Please connect to Soneium Minato
</span>

<button
className={styles.buttonSwitchChain}
onClick={() => switchChain({ chainId })}
>
Switch to Soneium Minato
</button>
</div>
)}
</div>
);
}

Test Out the App

With everything set up, it's time to test your dApp! This dApp allows users to connect their wallets, mint NFTs, and view their NFT balance directly from the interface. Users can see a list of their NFTs in the explorer (Example) and interact with Soneium Minato blockchain seamlessly.

Before Minting

When users first connect, they can see their current NFT balance:

During the Minting Process

While a minting transaction is in progress, the button will indicate that it’s processing, and the user can track the transaction status:

After Minting

Once minting is complete, a success message and a link to view the transaction on the Soneium Minato Explorer will be displayed:

Congratulations! 🎉

You’ve successfully built a fully functional NFT dApp on Soneium Minato using Next.js, WAGMI, and RainbowKit! The app can now seamlessly interact with the blockchain, mint NFTs, and explore their transactions.

Now that you have this foundation, the next step is to expand and customize your dApp. Whether you want to add more functionality, refine the user interface, or support additional chains, the possibilities are endless.

Happy building! 🚀