Fun Web3 Example Application!
In this article I will provide an overview (both use and code) of my completed Web3 application, Best Pet Poll. The application is intended to be a fun demo of Account Abstraction tools provided by thirdweb . It demonstrates how easy it is becoming to use blockchain apps without any knowledge of blockchain.
Vote For the Best Type of Pet!
The app is a simple poll; anyone can connect to the Poll as a guest, or using an email account, to cast a vote for the best type of pet. Votes are recorded on the blockchain, but no knowledge of blockchain is needed to cast a vote!
When the page loads, you will see the current tally for each type of pet, but there will be no voting buttons. You must click on the "Connect" button and connect to the site to vote.
The process of connecting will create an account on the blockchain (specifically, the Mumbai test blockchain). This allows your vote to be sent and recorded as a unique blockchain transaction.
Notes on Connecting:
Once connected, the voting buttons will appear. Click to vote! The process of writing your vote to the blockchain can take several seconds, during which a "working" indicator will be displayed on the button you clicked.
After your vote is recorded, the number of votes will increment on the page.
That's it! You used a blockchain!!
If this is the first time you intentionally used a blockchain app, let me know in the comments!
For Developers: A Look at the Code
This section is for developers who want to understand the use of thirdweb's tools for Account Abstraction.
The Connect Process
In a previous article, I described how to code the thirdweb embedded wallet connection used by this app. That article covered the code for the connect/disconnect button, as well as an overview of Account Abstraction. I will not rehash that part here.
On-Chain Interactions with Thirdweb's API
Once your app is connected to the blockchain, you can use the thirdweb API to interact with smart contracts. The first step is to get the desired smart contract from the blockchain. The pertinent lines of code are shown below, from my App.js component:
import { useContract } from "@thirdweb-dev/react";
import config from '../config.json';
function App() {
const [bestPetPollAddr, setBestPetPollAddr] = useState(null);
const [bestPetPoll, setBestPetPoll] = useState(null);
// ... connection code not shown ... //
// get the Best Pet Poll smart contract
if (!bestPetPollAddr) {
setBestPetPollAddr(config[chainId].bestPetPoll.address);
}
const { contract, isLoading } = useContract(bestPetPollAddr);
if (!isLoading && (contract !== bestPetPoll)) {
setBestPetPoll(contract);
}
Note that I first import useContract from thirdweb's react library. This function takes the smart contract address, and returns the contract to your application.
The above code stores the contract address and the returned contract in the React state as bestPetPollAddr and bestPetPoll respectively. Then I can pass them into the Poll component when rendering:
return (
<Container>
// ... not all code shown ... //
<Poll
walletAddress={walletAddress}
bestPetPollAddr={bestPetPollAddr}
bestPetPoll={bestPetPoll}
/>
<Footer />
</Container>
)
The Poll.js code is where all the on-chain action happens, so I will walk through all of it from top to bottom.
import React, { useState } from "react"
import {
useContractRead,
useContractWrite,
Web3Button
} from "@thirdweb-dev/react";
import './App.css'
import catImage from "../images/cat_640.jpg"
import dogImage from "../images/dog_640.jpg"
import horseImage from "../images/horse_640.jpg"
import fishImage from "../images/fish_640.jpg"
import birdImage from "../images/parrot_640.jpg"
import reptileImage from "../images/lizard_640.jpg"
// set an initial value for the vote counts
const initialVotes = {
Cat: 0,
Dog: 0,
Horse: 0,
Fish: 0,
Bird: 0,
Reptile: 0,
};
Importing and setup above. The important thing to note is that we make use of thirdweb's API and library with useContractRead, useContractWrite, and Web3Button.
Next comes the definition of the Poll component:
const Poll = ({ walletAddress, bestPetPollAddr, bestPetPoll }) => {
const [votes, setVotes] = useState(initialVotes);
var updatedVotes = votes;
var votesChanged = false;
In the code above, there are 3 variables for managing votes in the front-end.
We will come back to these variables in a moment.
Before doing anything else, we have to fetch the voting functions defined in the bestPetPoll smart contract on the blockchain. Since these functions write data to the blockchain (to record votes), we need the useContractWrite API call. It has the following format:
const { mutateAsync: frontEnd-function-name } =
useContractWrite(contract, "on-chain-function-name",);
It reaches out to the blockchain smart contract (which we got with the useContract API call earlier) to retrieve a certain named function in that contract, and assigns it to a variable in our front-end code. We can then call the smart contract function from our front-end.
The block of code below retrieves the voting function for each type of pet.
// get the contract's vote functions for later use
const { mutateAsync: voteForCatAsync } = useContractWrite(
bestPetPoll,
"voteForCat",
);
const { mutateAsync: voteForDogAsync } = useContractWrite(
bestPetPoll,
"voteForDog",
);
const { mutateAsync: voteForHorseAsync } = useContractWrite(
bestPetPoll,
"voteForHorse",
);
// ... remaining vote functions not shown ... //
In the above code, we can see that the bestPetPoll smart contract on the blockchain has functions called "voteForCat", "voteForDog", etc. These functions are defined in the Solidity code for the smart contract. (You can go to my GitHub account to find this smart contract code also ... the link is below.)
Now that we have the voting functions ready for our use, we will get the current vote tallies from the blockchain. To call a smart contract function that retrieves data from the blockchain we use the useContractRead API call, which has the format:
const { data: variable-name } =
useContractRead(contract, "on-chain-function-name",);
This calls a specific function in the smart contract, and stores the returned data in a variable for our use.
The next block of code is calling all the smart contract functions to get all the vote tallies from the blockchain:
//
//-------------------------------------------------------------//
// Get the current votes from the blockchain //
//-------------------------------------------------------------//
// get Cat Votes
const { data: catData } = useContractRead(
bestPetPoll,
"getCatVotes",
);
if (catData) {
const catVotes = catData.toNumber();
if (catVotes !== votes['Cat']) {
updatedVotes['Cat'] = catVotes;
votesChanged = true;
}
}
// get Dog votes
const { data: dogData } = useContractRead(
bestPetPoll,
"getDogVotes",
);
if (dogData) {
const dogVotes = dogData.toNumber();
if (dogVotes !== votes['Dog']) {
updatedVotes['Dog'] = dogVotes;
votesChanged = true;
}
}
// ... remaining reads not shown ... //
Note the logic around reading votes. After getting each tally from the smart contract, it has to convert it to a number with the toNumber() function. Then it checks to see if the retrieved tally is different than what we currently have stored in the front-end state. If so, it updates the value in updatedVotes and sets votesChanged to true.
After reading all the votes from the blockchain, we will know if any vote totals changed by checking the votesChanged value. If so, we set our stored votes object to the new updatedVotes as shown below.
//--------------------------------------------------------------//
// If any votes changed, update the App votes state //
//--------------------------------------------------------------//
//
if (votesChanged) {
setVotes(updatedVotes);
votesChanged = false;
}
With vote tallies updated, all that remains is to update the display of votes on the page! There are 2 columns of 3 pets on the page, and I show the code for the first column below:
return (
<div className="voting-section">
<div className="voting-column">
<div className="pet-label" key='Cat'>
<h3>Cat Votes: {votes['Cat']}</h3>
<img src={catImage} alt={`Cat logo`} className="pet-logo" />
{walletAddress ? (
<Web3Button
contractAddress={bestPetPollAddr}
action={() => voteForCatAsync({ args: [] })}
>
Vote for Cat
</Web3Button>
) : (
<div></div>
)}
</div>
<div className="pet-label" key='Dog'>
<h3>Dog Votes: {votes['Dog']}</h3>
<img src={dogImage} alt={`Dog logo`} className="pet-logo" />
{walletAddress ? (
<Web3Button
contractAddress={bestPetPollAddr}
action={() => voteForDogAsync({ args: [] })}
>
Vote for Dog
</Web3Button>
) : (
<div></div>
)}
</div>
<div className="pet-label" key='Horse'>
<h3>Horse Votes: {votes['Horse']}</h3>
<img src={horseImage} alt={`Horse logo`} className="pet-logo" />
{walletAddress ? (
<Web3Button
contractAddress={bestPetPollAddr}
action={() => voteForHorseAsync({ args: [] })}
>
Vote for Horse
</Web3Button>
) : (
<div></div>
)}
</div>
</div>
<div className="voting-column">
// ... 2nd display column code not shown ... //
</div>
</div>
);
}
export default Poll;
Things to note here in the render section:
And that is all it takes!
What About Blockchain Transaction Fees?
When the buttons are pressed, the thirdweb API takes care of interacting with the smart contract to call it's voting function, which results in a transaction on the blockchain. All transactions belong to the embedded wallet that was created when the user connected. Transaction fees are taken care of by the thirdweb infrastructure behind the scenes, and billed to the owner of the thirdweb API Key used to deploy the application.
In this case, I am using a test blockchain network, so there are no real fees. With a real-world application, the app owner may consider charging a membership/user fee (paid with a credit card) to cover any blockchain transaction costs behind the scenes. In that way the user would not need to own any crypto to use the application.
Full Project Code
To see the entire project, you can head over to my Best Pet Poll github repository. The information there will also link you to a separate repository which contains the smart contract code deployed on the blockchain.
Feel free to message or comment with any questions.
I would do some things differently if starting from scratch on this. (Like Next.js on the front end, and maybe a smart contract function to get all votes in a single call.)
Peace!