How to Build an Onchain Accountability App
Building an on-chain accountability app is a fun and creative way to leverage web3 technologies. In this tutorial, we'll walk through the process of creating a goal-setting application where users can deposit funds into a smart contract, set tasks to achieve their goals, and only withdraw their funds once all tasks are completed.
To get started, check out the full tutorial video:
Github repo:
Prerequisites
Before we begin, make sure you have the following:
- A thirdweb account
- A wallet like MetaMask to connect to thirdweb
- Some test funds on a testnet of choice
Step 1: Create the Smart Contract
First, let's create the smart contract that will handle the on-chain logic for our app. We'll be creating this contract from scratch with Solidity.
- Create a new contract by running
npx thirdweb create contract
in your command line and selectHardhat
orForge
as your framework. - Go to the Solidity file created.
contracts
folder. If using Forge this will be located in the src
folder.- Add the following code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
contract AccountabilityContract {
struct Task {
string description;
bool isCompleted;
}
Task[] public tasks;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can call this function");
_;
}
constructor() {
owner = msg.sender;
}
function createTask(string memory _description) public onlyOwner {
tasks.push(Task(_description, false));
}
function depositFunds() public payable onlyOwner {
require(msg.value > 0, "You need to send some ether");
}
function withdrawDepositSafely() public onlyOwner {
uint256 amount = address(this).balance;
require(amount > 0, "There are no funds to withdraw");
payable(owner).transfer(amount);
}
function allTasksCompleted() private view returns (bool) {
for (uint256 i = 0; i < tasks.length; i++) {
if (!tasks[i].isCompleted) {
return false;
}
}
return true;
}
function clearTasks() private {
delete tasks;
}
function completeTask(uint256 _taskId) public onlyOwner {
require(_taskId < tasks.length, "Task does not exist");
require(!tasks[_taskId].isCompleted, "Task is already completed");
tasks[_taskId].isCompleted = true;
if(allTasksCompleted()) {
uint256 amount = address(this).balance;
payable(owner).transfer(amount);
clearTasks();
}
}
function getTaskCount() public view returns (uint256) {
return tasks.length;
}
function getDeposit() public view returns (uint256) {
return address(this).balance;
}
function getTasks() public view returns (Task[] memory) {
return tasks;
}
}
Now that we have our smart contract created to handle our onchain functionality of our accountability app we can deploy it to any EVM blockchain.
Step 2: Deploy the Smart Contract
Next, we need to deploy the smart contract that will power our accountability app.
- Run the command
npx thirdweb deploy
to deploy your smart contract - Select the chain you want to deploy your contract to and click 'Deploy Now'
Once deployed, save the contract address somewhere as we'll need it later.
Step 3: Set Up the Next.js Project and install Connect SDK
Next, let's set up our Next.js project:
- Open a terminal and run:
npx thirdweb create app
- Select
Next.js
as the framework - Open project in code editor
- Edit the
.env
file and add yourCLIENT_ID
- Define your chain. In this example we'll be using Base Sepolia Testnet
import { defineChain } from "thirdweb";
import { baseSepolia } from "thirdweb/chains";
export const chain = defineChain( baseSepolia );
Step 4: Add a way to connect a wallet to app
Next, let's create a way for a user to connect a web3 wallet to our accountability app.
- In
page.tsx
we can add aConnectEmbed
component from Connect SDK
<div>
<ConnectEmbed
client={client}
chain={chain}
/>
</div>
- We can then check to see if a wallet is connected and if there is we can show a connected
ConnectButton
const account = useActiveAccount();
if(account) {
return (
<div>
<ConnectButton
client={client}
chain={chain}
/>
</div>
)
}
Now we have a way for our user to connect and disconnect a wallet from our accountability app.
Step 5: Build accountability app
Now, lets get some data from our smart contract and create our accountability app to interact with.
- Create a new file
contract.ts
in autils
folder - Get our accountability smart contract using
getContract
const contractAddress = "<Contract_Address>";
export const contract = getContract({
client: client,
chain: chain,
address: contractAddress,
abi: contractABI
});
- Create a new file for the accountability smart contract ABI
export const contractABI = ["Contract_ABI"] as const;
- Next, let's get the the deposit amount and task count from our smart contract
const { data: depositAmount } = useReadContract({
contract: contract,
method: "getDeposit",
});
const { data: taskCount } = useReadContract({
contract: contract,
method: "getTaskCount"
});
- Check to see if the user should deposit funds, create first task, or view their task list based on the deposit amount and task count
<div>
{depositAmount?.toString() === "0" && taskCount?.toString() === "0" ? (
<></>
) : depositAmount?.toString() !== "0" && taskCount?.toString() === "0" ? (
<></>
) : (
<></>
)}
</div>
- Create the component that will let a user deposit an amount of funds into the smart contract to hold until tasks are completed
export const Deposit = () => {
const [depositAmount, setDepositAmount] = useState(0);
return (
<div>
<h3>Deposit</h3>
<p>Please deposit the funds to hold.</p>
<input
type="number"
value={depositAmount}
onChange={(e) => setDepositAmount(Number(e.target.value))}
placeholder="0.0"
step={0.01}
/>
<TransactionButton
transaction={() => (
prepareContractCall({
contract: contract,
method: "despositFunds",
value: toWei(depositAmount.toString())
})
)}
onTransactionConfirmed={() => alert("Deposit successful!")}
>Deposit Funds</TransactionButton>
</div>
)
};
- Next, we can create our task list. If there are no task then we should prompt a user to enter their first task.
export const TaskList = () => {
const [task, setTask] = useState("");
const {
data: tasks,
isLoading: isLoadingTasks,
} = useReadContract({
contract: contract,
method: "getTasks"
});
return (
<div style={{ marginTop: "50px"}}>
{!isLoadingTasks && tasks!.length > 0 ? (
tasks?.map((task, index) => (
<TaskCard
key={index}
taskId={index}
task={task.description}
isCompleted={task.isCompleted}
/>
))
) : (
<div>
<h3>Create Task</h3>
<p>Please create the first task to complete.</p>
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Enter task..."
/>
<TransactionButton
transaction={() => (
prepareContractCall({
contract: contract,
method: "createTask",
params: [task]
})
)}
onTransactionConfirmed={() => {
setTask("");
alert("Task created successfully!");
}}
>Add Task</TransactionButton>
</div>
)}
</div>
)
};
- Create a TaskCard component that will show the data of a task along with a button to complete the task and mark it completed onchain
type TaskProps = {
taskId: number;
task: string;
isCompleted: boolean;
};
export const TaskCard = ({ taskId, task, isCompleted }: TaskProps) => {
return(
<div>
<p style={{ fontSize: "12px" }}>{task}</p>
{isCompleted ? (
<p>Done!</p>
) : (
<TransactionButton
transaction={() => (
prepareContractCall({
contract: contract,
method: "completeTask",
params: [BigInt(taskId)]
})
)}
onTransactionConfirmed={() => alert("Task completed!")}
>Complete Task</TransactionButton>
)}
</div>
)
};
- Finally, let's create a component to add a new task to our task list
export const AddTask = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [task, setTask] = useState("");
return (
<div>
<button
onClick={() => setIsModalOpen(true)}
>Add Task</button>
{isModalOpen && (
<div>
<div>
<button
onClick={() => setIsModalOpen(false)}
>Close</button>
<p>Enter task description:</p>
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Enter task..."
/>
<TransactionButton
transaction={() => (
prepareContractCall({
contract: contract,
method: "createTask",
params: [task]
})
)}
onTransactionConfirmed={() => {
setIsModalOpen(false);
setTask("");
alert("Task created successfully!");
}}
>Add Task</TransactionButton>
</div>
</div>
)}
</div>
)
};
Conclusion
And there you have it! We've built a web3 accountability app that allows users to:
- Connect their wallet
- Deposit any amount of funds to hold in the contract
- Create tasks to be completed
- Receive funds back once all tasks are completed
By using thirdweb's Connect SDK, we were able to easily interact with our smart contract to read data and make transactions.
You can build an app like this on any EVM-compatible blockchain, including popular L2s like Optimism, Base, Arbitrum, Avalanche and more.
The thirdweb Connect SDK provides a simple and powerful way to build web3 apps with a great developer experience.
I hope you enjoyed this tutorial and found it valuable!