šŸ› ļø Setting Up Your Frontend dApp with thirdweb for Rootstock

šŸ’Ŗ Prerequisites

Before starting, make sure you have the following:

  • Node.js (v16 or higher)

  • Package Manager: npm, yarn, or pnpm

  • MetaMask or any EVM-compatible wallet

  • Thirdweb Client ID: Get this from the thirdweb dashboard

  • Deployed smart contract on Rootstock testnet (chain ID 31) or mainnet (chain ID 30)

šŸ“¦ Step 1: Bootstrap Your Frontend with thirdweb (Part A - If you’re bootstrapping a new project from scratch)

Start by generating a project scaffold using thirdweb CLI:

npx thirdweb create

Follow the CLI prompts:

  • Project: App

  • Project name: thirdweb-app

  • Framework: Next.js

This will initialize your thirdweb-app and start to install dependencies

Then, open up your app in VS Code and run your server

cd thirdweb-app
code .
yarn dev

Part B - If you want to add thirdweb to your existing Nextjs app)

In your terminal, install thirdweb by running the command

npm install thirdweb

Then create a client.ts file and paste in the following code

import { createThirdwebClient } from "thirdweb";

// Replace this with your client ID string
// refer to https://portal.thirdweb.com/typescript/v5/client on how to get a client ID
const clientId = process.env.NEXT_PUBLIC_TEMPLATE_CLIENT_ID;

if (!clientId) {
  throw new Error("No client ID provided");
}

export const client = createThirdwebClient({
  clientId: clientId,
});

At the root of your application, wrap your app with a <ThirdwebProvider/> component. This keeps the state around like the active wallet and chain.

import "./globals.css";
import { ThirdwebProvider } from "thirdweb/react";

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

This guide is for setting up thirdweb for a Next.js project. To see setting up thirdweb for React (vite) projects, check here

🌐 Step 2: Define Rootstock Network Support

Create a new file chains/rootstock.ts and define Rootstock's mainnet and testnet configuration:

// chains/rootstock.ts
import { defineChain } from "thirdweb";

export const rootstockMainnet = defineChain({
  id: 30,
  name: "Rootstock Mainnet",
  rpc: "https://30.rpc.thirdweb.com",
  nativeCurrency: {
    name: "RBTC",
    symbol: "RBTC",
    decimals: 18,
  },
  blockExplorers: [
    { name: "RSK Explorer", url: "https://explorer.rsk.co" },
    { name: "Blockscout", url: "https://rootstock.blockscout.com" },
  ]
});

export const rootstockTestnet = defineChain({
  id: 31,
  name: "Rootstock Testnet",
  rpc: "https://public-node.testnet.rsk.co",
  nativeCurrency: {
    name: "tRBTC",
    symbol: "tRBTC",
    decimals: 18,
  },
  blockExplorers: [
    { name: "RSK Testnet Explorer", url: "https://explorer.testnet.rootstock.io/" },
    { name: "Blockscout Testnet Explorer", url: "https://rootstock-testnet.blockscout.com/" },
  ],
  testnet: true,
});

šŸ“ Step 3: Configure the thirdweb

Add your clientId to an .env file; if you’ve still not gotten your client ID, you can get it here

NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id

Now you can run your server by running

yarn dev

You should see something like this below

šŸ”Œ Step 4: Configure Connect Button for Rootstock Testnet

In your page.tsx, import your rootstockTestnet and add it as a chain to your <ConnectButton/>

"use client";

import Image from "next/image";
import { ConnectButton } from "thirdweb/react";
import thirdwebIcon from "@public/thirdweb.svg";
import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock"; //importing rooststockTestnet from ./chains/rootstock

export default function Home() {
  return (
    <main className="p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto">
      <div className="py-20">
        <Header />

        <div className="flex justify-center mb-20">
          <ConnectButton
            client={client}
            chain={rootstockTestnet} // we added rootstockTestnet as our default chain here
          />
        </div>
        // rest of your code 
      </div>
    </main>
  );
}

// rest of your code

The ConnectButton the component is themeable and supports 500+ wallets. Read here for more settings to configure the theme.

Now, head to your dApp and connect your wallet, and see how it goes

And that’s it, you have successfully integrated/set up thirdweb with your Next.js App. Now let’s see how we can do simple read and write functions to the blockchain using Thirdweb. For that, I’ve deployed a SimpleStorage contract

It has a set function, a write function, a get function, a read function, which will be the read and write functions we’ll explore with this contract. Here’s the contract address 0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA You can view it here on the rootstock testnet explorer

🧠 Step 5: Integrate Your Smart Contract

First, we access the currently connected account to the dApp, You can do that by using the useActiveAccount hook from thirdweb in your page.tsx

const account = useActiveAccount();

And then, based on this connected account, we can conditionally display the input field to update the stored string in the contract, and also display the current stored string in the contract.

      {account && (
          <div className="flex flex-col gap-4 mb-20">
            <h3 className="text-2xl font-medium mb-2">
              Stored String: {isLoading || isRefetching ? "fetching..." : storedString}
            </h3>
            <button
              onClick={() => refetch()}
              className="mb-4 px-4 py-2 border bg-gray-600 text-white rounded max-w-[140px] hover:bg-gray-700 transition-colors disabled:cursor-not-allowed bl"
              disabled={isLoading || isRefetching}
              >
                {isRefetching ? "Refreshing" : "Refresh string" }
            </button>
            <div className="flex flex-col gap-4">
              <span className="text-xl">Update stored string</span>
              <input
                type="text"
                value={newString}
                onChange={(e) => setNewString(e.target.value)}
                className="border border-zinc-700 rounded outline-none px-4 py-2 bg-zinc-900 text-zinc-300"
                placeholder="Enter new string"
              />
              <button
                onClick={() => handleSetStoredString(newString)}
                className="px-4 py-2 max-w-[140px] bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:cursor-not-allowed"
                disabled={updating || !newString}
              >
                {updating ? "Updating" : "Update"}
              </button>
            </div>
          </div>
        )}

Get Contract Instance

import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock";
import {
  getContract
} from "thirdweb";

const contract = getContract({
    address: "0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA",
    chain: rootstockTestnet,
    client,
});

Reading Data

import {
  useReadContract
} from "thirdweb/react";

 const { data: storedString, isPending: isLoading, refetch, isRefetching } = useReadContract({
    contract,
    method: "function get() external view returns (string memory)",
    params: [],
  });

Writing Data

import {
  prepareContractCall,
  sendAndConfirmTransaction,
} from "thirdweb";

const handleSetStoredString = async (newString: string) => {
    if (!account) return;
    try {
      setUpdating(true);

      const setTx = prepareContractCall({
        contract: contract,
        method: "function set(string memory _newString) external",
        params: [newString],
      });

      await sendAndConfirmTransaction({
        transaction: setTx,
        account,
      });

      setUpdating(false);
      setNewString("");
      alert("Update string on chain successul")
    } catch (error) {
      console.error();
      alert("An error occured");
      setUpdating(false);
    }
  };

Putting it all together.

"use client";

import Image from "next/image";
import {
  ConnectButton,
  useActiveAccount,
  useReadContract,
} from "thirdweb/react";
import thirdwebIcon from "@public/thirdweb.svg";
import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock";
import {
  getContract,
  prepareContractCall,
  sendAndConfirmTransaction,
} from "thirdweb";
import { useState } from "react";

export default function Home() {
 const account = useActiveAccount();
  const [newString, setNewString] = useState("");
  const [updating, setUpdating] = useState(false);

  const contract = getContract({
    address: "0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA",
    chain: rootstockTestnet,
    client,
  });

  const { data: storedString, isPending: isLoading } = useReadContract({
    contract,
    method: "function get() external view returns (string memory)",
    params: [],
  });

 const handleSetStoredString = async (newString: string) => {
    if (!account) return;
    try {
      setUpdating(true);

      const setTx = prepareContractCall({
        contract: contract,
        method: "function set(string memory _newString) external",
        params: [newString],
      });

      await sendAndConfirmTransaction({
        transaction: setTx,
        account,
      });

      setUpdating(false);
      setNewString("");
      alert("Update string on chain successul")
    } catch (error) {
      console.error();
      alert("An error occured");
      setUpdating(false);
    }
  };

  return (
    <main className="p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto">
      <div className="py-20">
        <Header />

        <div className="flex justify-center mb-16">
          <ConnectButton client={client} chain={rootstockTestnet} />
        </div>

        {account && (
          <div className="flex flex-col gap-4 mb-20">
            <h3 className="text-2xl font-medium mb-2">
              Stored String: {isLoading || isRefetching ? "fetching..." : storedString}
            </h3>
            <button
              onClick={() => refetch()}
              className="mb-4 px-4 py-2 border bg-gray-600 text-white rounded max-w-[140px] hover:bg-gray-700 transition-colors disabled:cursor-not-allowed bl"
              disabled={isLoading || isRefetching}
              >
                {isRefetching ? "Refreshing" : "Refresh string" }
            </button>
            <div className="flex flex-col gap-4">
              <span className="text-xl">Update stored string</span>
              <input
                type="text"
                value={newString}
                onChange={(e) => setNewString(e.target.value)}
                className="border border-zinc-700 rounded outline-none px-4 py-2 bg-zinc-900 text-zinc-300"
                placeholder="Enter new string"
              />
              <button
                onClick={() => handleSetStoredString(newString)}
                className="px-4 py-2 max-w-[140px] bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:cursor-not-allowed"
                disabled={updating || !newString}
              >
                {updating ? "Updating" : "Update"}
              </button>
            </div>
          </div>
        )}


        // rest of your code 
      </div>
    </main>
  );
}

//rest of your code

Now, let’s test,

First, you should see something like this,

Enter your new string and fire the write transaction to update the string and confirm the transaction in your wallet

Updating the storedString in the contract was successful

Now we refresh

And now we have the updated string being displayed

šŸ”¹ Final Thoughts

In conclusion, setting up a frontend dApp with thirdweb for Rootstock is a streamlined process that empowers developers to quickly build secure and user-friendly applications. By leveraging thirdweb’s SDKs and the new Rootstock support, you can efficiently connect your frontend to deployed smart contracts with minimal boilerplate. Whether experimenting on testnet or going live on mainnet, this setup ensures you're ready to ship fast.

The full working code for this tutorial can be found here https://github.com/michojekunle/Nextjs-Thirdweb-RSK-Setup, I’ve also deployed it live here.

Be sure to consult the official Rootstock docs and thirdweb docs for more options and extensions! Join the Rootstock community on Discord. Happy Coding 🄳!

11
Subscribe to my newsletter

Read articles from Michael Ojekunle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Michael Ojekunle
Michael Ojekunle

I am an Enthusiastic, curious and imaginative Web Developer, eager to contribute to team success through hard work, Attention to detail and Excellent Organizational Skills, always open to new and unconventional ideas. I take my work as a Web Developer seriously and this means I always ensure my skills are kept up to date within this rapidly changing industry.