Build a full-stack Task Manager dApp on Rootstock using Next.js, Wagmi, rainbowkit and Foundry

Hi builders, welcome back to yet another exciting tutorial. In this guide, we will build a full-stack Task Manager Dapp, fully decentralised on the Rootstock network. The Rootstock network offers a powerful sidechain to Bitcoin, bringing Ethereum-compatible smart contracts to the Bitcoin ecosystem. This tutorial covers how to create a full-stack decentralised application on the Rootstock network, illustrating core concepts, best practices, and tools required for building, deploying, and interacting with smart contracts on Rootstock. Leveraging the latest tools such as Foundry for smart contract development, React frameworks like Next.js, and modern Ethereum frontend libraries wagmi, viem, and RainbowKit, you can create scalable, responsive, and highly interactive decentralised applications tailored for the Rootstock ecosystem. There are many Blockchains available in the market, but why should you build on Rootstock? I think this is very important to cover, so let’s dive in.
Why Build on Rootstock?
Rootstock is an Ethereum-compatible smart contract platform secured by the Bitcoin network's hash power. It combines Bitcoin’s security with the programmability of Ethereum, making it ideal for developers seeking to build powerful decentralised apps with the best of both worlds.
Key advantages include:
Security: Secured by merged mining on Bitcoin’s hash power.
Compatibility: EVM support, enabling Solidity smart contracts and familiar Ethereum tooling.
Low Fees and Speed: Faster block times and lower fees compared to the Ethereum mainnet.
Growing Ecosystem: Increasing wallet and infrastructure support.
Why This Stack?
The toolset chosen represents cutting-edge standards
Foundry: Lightning-fast smart contract development framework with great Solidity testing.
Next.js: Production-ready React framework optimised for SEO, performance, and flexibility.
Tailwind CSS: Utility-first CSS framework enabling rapid and consistent UI styling with responsiveness and accessibility in mind.
Wagmi & Viem: Modern React hooks and utilities to manage Web3 connections and blockchain calls elegantly.
RainbowKit: Beautiful and accessible React wallet connection toolkit providing an excellent user experience.
Prerequisites
Before starting, ensure you have:
- Foundry installed (Run
curl -L https://foundry.paradigm.xyz | bash&foundryupon Unix systems)
Follow this installation guide https://getfoundry.sh/introduction/installation
Node.js (v16 or later) and npm/yarn ( we will use NPM ) https://www.npmjs.com/
MetaMask or compatible Wallet Extension installed and configured for Rootstock networks
Basic understanding of React and Solidity smart contracts.
Now, without wasting time, let’s start building our Task Manager Dapp fully on Rootstock chain.
Create a new directory TaskManager and open this directory in your code editor (VS Code recommended )
We will split and build our dapp into 2 parts, the smart contract backend and the Next.js frontend. First, we are going to build a smart contract, we will test it, and then we will deploy it on the Rootstock network. Then we will build the frontend and connect our smart contract to the frontend and at the end, we will have a fully working Task manager dapp on Rootstock where users can add Tasks, Mark as completed and delete their tasks and all this is handled by our smart contract deployed on the Rootstock network.
You are in TaskManager directory. Create two new directories and name them contract and frontend Now open your terminal and go to contract directory as I said earlier, we will build a smart contract first.
Run the following command to go into the contract directory and initialise the foundry project.
cd contract
forge init
You will see your foundry project initialised with some folders and files.
Now go ahead and delete the files counter.sol from src folder counter.s.sol from script folder and counter.t.sol from test folder . Great We will start everything from scratch. Now create a new file TaskManager.sol in the src directory and paste the following smart contract code.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// warning - Don't use this code in production.
contract TaskManager {
struct Task {
uint256 id;
string description;
bool completed;
address owner;
uint256 createdAt;
}
mapping(uint256 => Task) public tasks;
mapping(address => uint256[]) public userTasks;
uint256 public taskCounter;
event TaskCreated(uint256 indexed taskId, address indexed owner, string description);
event TaskCompleted(uint256 indexed taskId, address indexed owner);
event TaskDeleted(uint256 indexed taskId, address indexed owner);
modifier onlyTaskOwner(uint256 _taskId) {
require(tasks[_taskId].owner == msg.sender, "Not task owner");
_;
}
modifier taskExists(uint256 _taskId) {
require(tasks[_taskId].owner != address(0), "Task does not exist");
_;
}
function createTask(string memory _description) external {
require(bytes(_description).length > 0, "Description cannot be empty");
taskCounter++;
tasks[taskCounter] = Task({
id: taskCounter,
description: _description,
completed: false,
owner: msg.sender,
createdAt: block.timestamp
});
userTasks[msg.sender].push(taskCounter);
emit TaskCreated(taskCounter, msg.sender, _description);
}
function completeTask(uint256 _taskId) external taskExists(_taskId) onlyTaskOwner(_taskId) {
require(!tasks[_taskId].completed, "Task already completed");
tasks[_taskId].completed = true;
emit TaskCompleted(_taskId, msg.sender);
}
function deleteTask(uint256 _taskId) external taskExists(_taskId) onlyTaskOwner(_taskId) {
// Remove from user's task array
uint256[] storage userTaskArray = userTasks[msg.sender];
for (uint256 i = 0; i < userTaskArray.length; i++) {
if (userTaskArray[i] == _taskId) {
userTaskArray[i] = userTaskArray[userTaskArray.length - 1];
userTaskArray.pop();
break;
}
}
delete tasks[_taskId];
emit TaskDeleted(_taskId, msg.sender);
}
function getUserTasks(address _user) external view returns (uint256[] memory) {
return userTasks[_user];
}
function getTask(uint256 _taskId) external view returns (Task memory) {
return tasks[_taskId];
}
function getUserTaskDetails(address _user) external view returns (Task[] memory) {
uint256[] memory taskIds = userTasks[_user];
Task[] memory userTaskDetails = new Task[](taskIds.length);
for (uint256 i = 0; i < taskIds.length; i++) {
userTaskDetails[i] = tasks[taskIds[i]];
}
return userTaskDetails;
}
}
Great! Now let’s compile our contract and check if anything is wrong there. To compile the contract run the following command.
forge build
You will see our contract is compiled successfully. ( see the image below )
Now let’s test our smart contract to ensure our smart contract is working and behaving correctly as we expected. Create a new file in the test directory and name it TaskManager.t.sol, and paste the following code in this file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {TaskManager} from "../src/TaskManager.sol";
contract TaskManagerTest is Test {
TaskManager public taskManager;
address public user1 = address(0x1);
address public user2 = address(0x2);
function setUp() public {
taskManager = new TaskManager();
}
function testCreateTask() public {
vm.prank(user1);
taskManager.createTask("Complete tutorial");
TaskManager.Task memory task = taskManager.getTask(1);
assertEq(task.id, 1);
assertEq(task.description, "Complete tutorial");
assertEq(task.completed, false);
assertEq(task.owner, user1);
}
function testCompleteTask() public {
vm.prank(user1);
taskManager.createTask("Complete tutorial");
vm.prank(user1);
taskManager.completeTask(1);
TaskManager.Task memory task = taskManager.getTask(1);
assertTrue(task.completed);
}
function testDeleteTask() public {
vm.prank(user1);
taskManager.createTask("Complete tutorial");
vm.prank(user1);
taskManager.deleteTask(1);
TaskManager.Task memory task = taskManager.getTask(1);
assertEq(task.owner, address(0));
}
function testOnlyOwnerCanModifyTask() public {
vm.prank(user1);
taskManager.createTask("Complete tutorial");
vm.prank(user2);
vm.expectRevert("Not task owner");
taskManager.completeTask(1);
}
}
Now run the following command to check our tests.
forge test
And there we go… our all 4 tests are passed successfully! ✅ ( see the image below )
Amazing, you did it! We built and tested our smart contract and now its time time to deploy our contract. Go inside script directory and create a new file Deploy.s.sol and paste the following solidity scripting code in this file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import {TaskManager} from "../src/TaskManager.sol";
contract DeployScript is Script {
function run() external {
vm.startBroadcast();
TaskManager taskManager = new TaskManager();
console.log("TaskManager deployed to:", address(taskManager));
vm.stopBroadcast();
}
}
Great! We are almost ready to deploy our TaskManager contract on the Rootstock test network, but before moving ahead, we need to do some configuration in our foundry.toml file.
Update your foundry.toml file as following:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.20"
evm_version = "london"
[rpc_endpoints]
rootstock_testnet="https://public-node.testnet.rsk.co"
rootstock_mainnet = "https://public-node.rsk.co"
Amazing!!! All set. Now we are finally ready to deploy your contract on Rootstock network. Ensure you have a Rootstock wallet set up and funded with Rootstock tokens for transaction fees.
To add the Rootstock network to your wallet, follow the link: https://dev.rootstock.io/dev-tools/wallets/metamask/
To get Rootstock faucets, follow the link: https://faucet.rootstock.io/
Make sure you set up and imported your wallet in Foundry Key Store: read here - https://book.getfoundry.sh/reference/cast/cast-wallet-import
To deploy our contract, go ahead and open your terminal (make sure you are in the contract directory) and run the following command.
forge script script/Deploy.s.sol:DeployScript --rpc-url rootstock_testnet --account YOUR_WALLET_NAME --sender YOUR_WALLET_ADDRESS --broadcast --legacy
Example:
forge script script/Deploy.s.sol:DeployScript --rpc-url rootstock_testnet --account pandit --sender 0x339abb297eB21A0ee52E22e07DDe496c0fe98fB9 --broadcast --legacy
It will ask you for the password you set up during importing your wallet in Foundry Keystore. Enter the password and Boom. You will see our contract is deployed on the Rootstock test network. ( see the image below )
Great! We have successfully built, tested, and deployed our TaskManager contract on the Rootstock test Network.
Now you can visit the newly upgraded Rootstock testnet block explorer https://explorer.testnet.rootstock.io/ and check whether our smart contract is indeed deployed or not.
Copy the contract’s address from your terminal and paste it in the search bar on the testnet block explorer. ( see the image below )
And now it’s time to build our UI, connect our smart contract to the frontend and build an actual dapp. ( Don’t kill the terminal ) Keep it running or copy the contract address we deployed and keep it safe anywhere.
Let’s start working on our frontend and UI part, which is part 2 of our dapp, hope you remember, I mentioned earlier.
Go back to your main directory and then go into frontend directory in your terminal and initialise our NextJs project.
cd ..
cd frontend
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
Check NO to Turbo pack and hit Enter or return, and you will see your NextJs project is initialised in frontend directory. Great, open your frontend directory and you will see the folder structure like ( see the image below )
That’s not enough; we also need to install external dependencies to build this app.What dependencies? Wagmi (React hooks for Ethereum applications) - https://wagmi.sh/
Viem ( interacting with the blockchain ) - https://viem.sh/
RainbowKit(for wallet managment) - https://rainbowkit.com/docs/introduction
Install these by running the following command.
npm install @rainbow-me/rainbowkit wagmi viem@2.x @tanstack/react-query
Now go in to src directory and create a new directory named config, and in this directory create a new file wagmi.ts and paste the following code in this file.
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { rootstockTestnet } from 'wagmi/chains';
// Define Rootstock Testnet if not available
const rootstockTestnetCustom = {
id: 31,
name: 'Rootstock Testnet',
nativeCurrency: {
decimals: 18,
name: 'Smart Bitcoin',
symbol: 'trBTC',
},
rpcUrls: {
public: { http: ['https://public-node.testnet.rsk.co'] },
default: { http: ['https://public-node.testnet.rsk.co'] },
},
blockExplorers: {
Rsk_Explorer: {
name: 'RSK Testnet Explorer',
url: 'https://explorer.testnet.rsk.co'
},
default: {
name: 'RSK Testnet Explorer',
url: 'https://explorer.testnet.rsk.co'
},
},
} as const;
export const config = getDefaultConfig({
appName: 'Rootstock TaskManager',
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || 'your-project-id',
chains: [rootstockTestnetCustom],
ssr: true,
});
Cool, we just configured wagmi, and now we have to create the contract configuration. To do this, create a new file in the same config directory contract.ts and paste the following code in this file. ( Replace your contract address you deployed with my deployed contract address )
export const CONTRACT_ADDRESS = '0x9d9fc901299E1a15b699ca70454628Fd09E8Ff33' as const; // Replace with your deployed contract address
export const CONTRACT_ABI = [
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256",
"indexed": true
},
{
"internalType": "address",
"name": "owner",
"type": "address",
"indexed": true
}
],
"type": "event",
"name": "TaskCompleted",
"anonymous": false
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256",
"indexed": true
},
{
"internalType": "address",
"name": "owner",
"type": "address",
"indexed": true
},
{
"internalType": "string",
"name": "description",
"type": "string",
"indexed": false
}
],
"type": "event",
"name": "TaskCreated",
"anonymous": false
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256",
"indexed": true
},
{
"internalType": "address",
"name": "owner",
"type": "address",
"indexed": true
}
],
"type": "event",
"name": "TaskDeleted",
"anonymous": false
},
{
"inputs": [
{ "internalType": "uint256", "name": "_taskId", "type": "uint256" }
],
"stateMutability": "nonpayable",
"type": "function",
"name": "completeTask"
},
{
"inputs": [
{
"internalType": "string",
"name": "_description",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "function",
"name": "createTask"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_taskId", "type": "uint256" }
],
"stateMutability": "nonpayable",
"type": "function",
"name": "deleteTask"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_taskId", "type": "uint256" }
],
"stateMutability": "view",
"type": "function",
"name": "getTask",
"outputs": [
{
"internalType": "struct TaskManager.Task",
"name": "",
"type": "tuple",
"components": [
{ "internalType": "uint256", "name": "id", "type": "uint256" },
{
"internalType": "string",
"name": "description",
"type": "string"
},
{ "internalType": "bool", "name": "completed", "type": "bool" },
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
]
}
]
},
{
"inputs": [
{ "internalType": "address", "name": "_user", "type": "address" }
],
"stateMutability": "view",
"type": "function",
"name": "getUserTaskDetails",
"outputs": [
{
"internalType": "struct TaskManager.Task[]",
"name": "",
"type": "tuple[]",
"components": [
{ "internalType": "uint256", "name": "id", "type": "uint256" },
{
"internalType": "string",
"name": "description",
"type": "string"
},
{ "internalType": "bool", "name": "completed", "type": "bool" },
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
]
}
]
},
{
"inputs": [
{ "internalType": "address", "name": "_user", "type": "address" }
],
"stateMutability": "view",
"type": "function",
"name": "getUserTasks",
"outputs": [
{ "internalType": "uint256[]", "name": "", "type": "uint256[]" }
]
},
{
"inputs": [],
"stateMutability": "view",
"type": "function",
"name": "taskCounter",
"outputs": [
{ "internalType": "uint256", "name": "", "type": "uint256" }
]
},
{
"inputs": [
{ "internalType": "uint256", "name": "", "type": "uint256" }
],
"stateMutability": "view",
"type": "function",
"name": "tasks",
"outputs": [
{ "internalType": "uint256", "name": "id", "type": "uint256" },
{
"internalType": "string",
"name": "description",
"type": "string"
},
{ "internalType": "bool", "name": "completed", "type": "bool" },
{ "internalType": "address", "name": "owner", "type": "address" },
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
]
},
{
"inputs": [
{ "internalType": "address", "name": "", "type": "address" },
{ "internalType": "uint256", "name": "", "type": "uint256" }
],
"stateMutability": "view",
"type": "function",
"name": "userTasks",
"outputs": [
{ "internalType": "uint256", "name": "", "type": "uint256" }
]
}
] as const;
woah! We did it. Now lets move ahead for wallet integrations, Lets set up our providers.
Go into src/app/layout.tsx and remove everything we already have, which comes with Nextjs and update this file with the following code.
'use client';
import './globals.css';
import '@rainbow-me/rainbowkit/styles.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { config } from '@/config/wagmi';
const queryClient = new QueryClient();
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</body>
</html>
);
}
This is amazing. We just set up our provider and wrapped our dapp with rainbowkit and wagmi which we previously installed. Now let’s create a component and start building a basic UI for our dapp. Let’s create a Header first for our dapp. Create a new directory named components in src directory, and inside components create a new file Header.tsx and paste the following code in this file.
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
export default function Header() {
return (
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<div className="flex items-center">
<h1 className="text-2xl font-bold text-gray-900">
TaskManager dApp
</h1>
<span className="ml-2 px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded-full">
Rootstock
</span>
</div>
<ConnectButton />
</div>
</div>
</header>
);
}
Our dapps Header is done, and now let’s create contract interactions and some hooks. Let’s first create a Task hook. Go back to src directory and create a new directory hooks and in this directory create a new file useTasks.ts and paste the following code in this file.
'use client';
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { CONTRACT_ADDRESS, CONTRACT_ABI } from '@/config/contract';
import { useState, useEffect } from 'react';
export interface Task {
id: bigint;
description: string;
completed: boolean;
owner: string;
createdAt: bigint;
}
export function useTasks() {
const { address } = useAccount();
const [tasks, setTasks] = useState<Task[]>([]);
const { data: tasksData, refetch: refetchTasks } = useReadContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'getUserTaskDetails',
args: address ? [address] : undefined,
query: {
enabled: !!address,
},
});
const { writeContract: createTaskWrite, data: createTaskHash } = useWriteContract();
const { writeContract: completeTaskWrite, data: completeTaskHash } = useWriteContract();
const { writeContract: deleteTaskWrite, data: deleteTaskHash } = useWriteContract();
const { isLoading: isCreateLoading } = useWaitForTransactionReceipt({
hash: createTaskHash,
});
const { isLoading: isCompleteLoading } = useWaitForTransactionReceipt({
hash: completeTaskHash,
});
const { isLoading: isDeleteLoading } = useWaitForTransactionReceipt({
hash: deleteTaskHash,
});
useEffect(() => {
if (tasksData) {
setTasks(tasksData as Task[]);
}
}, [tasksData]);
const createTask = async (description: string) => {
if (!address) return;
try {
createTaskWrite({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'createTask',
args: [description],
});
} catch (error) {
console.error('Error creating task:', error);
}
};
const completeTask = async (taskId: bigint) => {
if (!address) return;
try {
completeTaskWrite({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'completeTask',
args: [taskId],
});
} catch (error) {
console.error('Error completing task:', error);
}
};
const deleteTask = async (taskId: bigint) => {
if (!address) return;
try {
deleteTaskWrite({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'deleteTask',
args: [taskId],
});
} catch (error) {
console.error('Error deleting task:', error);
}
};
// Refetch tasks after successful transactions
useEffect(() => {
if (createTaskHash || completeTaskHash || deleteTaskHash) {
setTimeout(() => refetchTasks(), 2000);
}
}, [createTaskHash, completeTaskHash, deleteTaskHash, refetchTasks]);
return {
tasks,
createTask,
completeTask,
deleteTask,
refetchTasks,
isCreateLoading,
isCompleteLoading,
isDeleteLoading,
};
}
Amazing! We hooked and created a hook. Now let’s create a task form. Go back to the components directory and create a new file TaskForm.tsx, and paste the following code into this file.
'use client';
import { useState } from 'react';
import { useTasks } from '@/hooks/useTasks';
export default function TaskForm() {
const [description, setDescription] = useState('');
const { createTask, isCreateLoading } = useTasks();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!description.trim()) return;
await createTask(description);
setDescription('');
};
return (
<form onSubmit={handleSubmit} className="mb-8">
<div className="flex gap-4">
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter task description..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={isCreateLoading}
/>
<button
type="submit"
disabled={isCreateLoading || !description.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isCreateLoading ? 'Creating...' : 'Add Task'}
</button>
</div>
</form>
);
}
We also need to create another component to list our tasks, for that let's create another file in the same components directory TaskList.tsx and paste the following code in this file.
'use client';
import { useTasks, Task } from '@/hooks/useTasks';
import { useAccount } from 'wagmi';
export default function TaskList() {
const { address } = useAccount();
const { tasks, completeTask, deleteTask, isCompleteLoading, isDeleteLoading } = useTasks();
if (!address) {
return (
<div className="text-center py-8 text-gray-500">
Please connect your wallet to view tasks
</div>
);
}
if (tasks.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
No tasks yet. Create your first task above!
</div>
);
}
return (
<div className="space-y-4">
{tasks.map((task: Task) => (
<div
key={task.id.toString()}
className={`p-4 border rounded-lg ${task.completed ? 'bg-green-50 border-green-200' : 'bg-white border-gray-200'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<p
className={`text-lg ${task.completed ? 'line-through text-gray-500' : 'text-gray-900'
}`}
>
{task.description}
</p>
<p className="text-sm text-gray-500 mt-1">
Created: {new Date(Number(task.createdAt) * 1000).toLocaleDateString()}
</p>
</div>
<div className="flex gap-2">
{!task.completed && (
<button
onClick={() => completeTask(task.id)}
disabled={isCompleteLoading}
className="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
Complete
</button>
)}
<button
onClick={() => deleteTask(task.id)}
disabled={isDeleteLoading}
className="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50"
>
Delete
</button>
</div>
</div>
</div>
))}
</div>
);
}
Yaaayyy! We finished writing code for the components. Now go back to the main page component which means src/app/page.tsx, and update the page.tsx with the following code. ( Make sure you are clearing all previous code in this file. )
'use client';
import Header from '@/components/Header';
import TaskForm from '@/components/TaskForm';
import TaskList from '@/components/TaskList';
import { useAccount } from 'wagmi';
export default function Home() {
const { isConnected } = useAccount();
return (
<div className="min-h-screen bg-gray-50">
<Header />
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Your Tasks
</h2>
{isConnected ? (
<>
<TaskForm />
<TaskList />
</>
) : (
<div className="text-center py-12">
<h3 className="text-xl text-gray-600 mb-4">
Welcome to TaskManager dApp
</h3>
<p className="text-gray-500 mb-6">
Connect your wallet to start managing your tasks on the Rootstock network
</p>
</div>
)}
</div>
</main>
</div>
);
}
Great! We have almost finished building our full-stack dapp. We have to do one more task, and we will run the dapp. Go ahead and create a .env.local file at the root of the frontend directory and declare the following variables in this file.
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="your_wallet_connect_project_id"
At this point, your folder structure should look like this. ( see the image below )
Visit the https://cloud.reown.com/sign-in and sign in with your wallet or EMail and now you’re in your dashboard page. At the right top header there create button, click that button and create a new project, fill the project name and click on continue. Then select AppKit and then Nextjs as a platform and then click on create to get an api key.
Now you will see sidebar left side of the page there project ID on top right below your account name.
Copy this project ID from there and come back to our VS Code .env.local file, paste this project ID right front of NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID variable we declared in our .env.local file.
Example:
Amazing! We are ready. Now open your terminal ( ensure you are in frontend directory ) and run the following command to run our app.
npm run dev
And, there we go… boom, you will be asked to visit http://localhost:3000/ to see your app running. Go click on this link and see that your app is indeed running and your initial UI looks like this (see the image below)
Make sure you are on the Rootstock test network in your wallet; otherwise, it will warn you you are on the wrong network.
What next? Connect your wallet to our dapp if you have not already connected. Try adding tasks and completing them, and then deleting them; everything will happen on the Rootstock chain. Make sure you have enough faucets in your account.
Further, you can deploy this dapp on your favourite deployment platform like Vercel or netlify and make it LIVE.
What did you achieve following this tutorial? You've successfully built a full-stack dApp on Rootstock with
✅ Smart contract development using Foundry
✅ Contract deployment to Rootstock network
✅ Modern React frontend with Next.js
✅ Wallet integration with RainbowKit
✅ Contract interaction using Wagmi and Viem
✅ Type-safe development with TypeScript
This TaskManager dApp demonstrates core dApp functionality, including wallet connection, contract interaction, and real-time updates. You can extend this foundation to build more complex applications on Rootstock.
How can you improve this further?
Add more complex smart contract features
Implement IPFS for decentralised storage
Add user authentication and profiles
Integrate with DeFi protocols on Rootstock
Add mobile-responsive design improvements
Implement advanced state management with Zustand or Redux
Wrapping this up! massive congratulations on building a full-stack dapp on Rootstock network and now you have a pretty good understanding of how you can build dapps on Rootstock network using same EVM stack.
If you're stuck anywhere, follow the GitHub repo (link below) for the entire source code and feel free to ask for help in the Rootstock community. Join the Discord and Telegram communities using the following links:
Source code - https://github.com/panditdhamdhere/Task-manager
Roostock Discord: http://discord.gg/rootstock
Rootstock Telegram: @rskofficialcommunity
Rootstock Docs: https://dev.rootstock.io/





