Build Your First dApp
This module walks you through building a simple decentralized application (dApp) on Rootstock. You'll integrate a deployed smart contract with a frontend using Ethers.js and a wallet provider (MetaMask or Rabby). By the end, you'll have a fully functional dApp where users can read/write data on the Rootstock Testnet.
Overview
You will build a minimal dApp that:
- Connects to Rootstock Testnet
- Lets users connect their wallet
- Interacts with a simple smart contract (HelloRootstock.sol)
- Reads stored data from the blockchain
- Updates contract state using a transaction
This is the first "real" end-to-end Rootstock project — contract, deployment, and frontend.
Prerequisites
Before starting, ensure you have:
- A deployed contract on Rootstock Testnet (from the previous module)
- Node.js installed
- Basic familiarity with React or Next.js
- MetaMask or Rabby wallet configured for Rootstock Testnet
- Your contract's:
- address
- ABI
- Testnet RPC
Project Setup (Next.js)
Create a fresh project:
npx create-next-app rootstock-dApp
cd rootstock-dApp
npm install ethers
This gives you a clean React/Next environment with zero noise.
Add Your Contract ABI
Inside your project, create:
/src/abi/HelloRootstock.json
Paste your contract ABI from the compiled Hardhat/Foundry output:
[
{
"inputs": [],
"name": "message",
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "string", "name": "_msg", "type": "string" }],
"name": "setMessage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
Wallet Connection (Ethers.js)
Create a reusable wallet connection hook:
/src/hooks/useWallet.js
import { useState } from "react";
import { ethers } from "ethers";
export default function useWallet() {
const [account, setAccount] = useState(null);
async function connect() {
if (!window.ethereum) return alert("No wallet found");
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAccount(accounts[0]);
}
const provider = typeof window !== "undefined" && window.ethereum
? new ethers.BrowserProvider(window.ethereum)
: null;
return { account, connect, provider };
}
Contract Interaction Code
Create a helper file:
/src/lib/contract.js
import { ethers } from "ethers";
import abi from "../abi/HelloRootstock.json";
const CONTRACT_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
const RPC_URL = "https://public-node.testnet.rsk.co";
export function getReadProvider() {
return new ethers.JsonRpcProvider(RPC_URL);
}
export function getContract(provider) {
return new ethers.Contract(CONTRACT_ADDRESS, abi, provider);
}
dApp UI (Read + Write)
Modify your homepage:
/src/app/page.js
"use client";
import useWallet from "../hooks/useWallet";
import { getReadProvider, getContract } from "../lib/contract";
import { useEffect, useState } from "react";
export default function Home() {
const { account, connect, provider } = useWallet();
const [message, setMessage] = useState("");
const [newMessage, setNewMessage] = useState("");
async function loadMessage() {
const readProvider = getReadProvider();
const contract = getContract(readProvider);
const msg = await contract.message();
setMessage(msg);
}
async function updateMessage() {
if (!provider) return alert("Wallet not connected");
const signer = await provider.getSigner();
const contract = getContract(signer);
const tx = await contract.setMessage(newMessage);
await tx.wait();
loadMessage();
}
useEffect(() => {
loadMessage();
}, []);
return (
<main style={{ padding: 40 }}>
<h1>Hello Rootstock dApp</h1>
{account ? (
<p>Connected: {account}</p>
) : (
<button onClick={connect}>Connect Wallet</button>
)}
<h2>Stored Message:</h2>
<p>{message}</p>
<input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Enter new message"
/>
<button onClick={updateMessage}>Update Message</button>
</main>
);
}
Run the dApp
Start the local server:
npm run dev
Open:
http://localhost:3000
You should now be able to:
- Connect wallet
- Read message from Rootstock Testnet
- Write/update on-chain
- See the value update live
Troubleshooting
❗ Wallet doesn't connect
Ensure Rootstock Testnet is configured in your MetaMask:
- Chain ID: 31
- Currency: trBTC
- RPC: https://public-node.testnet.rsk.co
❗ Transaction fails
Check your Testnet balance:
- You must have trBTC from the faucet (covered earlier).
❗ Read works but write doesn't
Your wallet may not be connected as signer. Verify the provider:
const signer = await provider.getSigner();
For further practice, check out the dApp Tutorial.
Summary
In this module you learned how to:
- Build a simple Rootstock dApp using Next.js
- Connect a wallet using Ethers.js
- Read contract state from Rootstock Testnet
- Send transactions to update contract data
- Structure frontend + blockchain interactions cleanly
This module transitions a learner from contract developer to full dApp builder — the skillset required for all advanced Rootstock applications.