How to Build a Voting dApp on Tezos with SmartPy and React — Part 2
If you made it through Part 1, well done! If you stumbled on this without reading Part 1, read it here now!
We’ll continue building a dApp that lets users of our app vote for their favorite football players in 2023 — the Ballon tz’or!
In this part of the tutorial, we’ll be using NextJS (a React framework), Taquito — a JavaScript framework for interacting with the Tezos blockchain and Beacon SDK — a library for connecting to wallets. We’ll be using these tools within our codebase to interact with our smart contract.
TL;DR
Here for the code only? See the full code for this tutorial here
See the full smart contract code here
Getting Started
To get us up and running quickly, I have set up a NextJS repo you can clone.
Run the command
git clone --branch example-branch https://github.com/onedebos/ballon-tz-or.git
&& cd ballon-tz-or
Once done, run the command npm install
to make sure all the libraries we need are installed.
The commands above will initialize our project with the beacon-wallet and taquito libraries.
Libraries Breakdown
- Taquito — Taquito allows us to interact with data on the blockchain. We can get data from the Blockchain, send information to smart contracts, and more.
- Beacon Wallet — The Beacon Wallet library allows our dApp to connect to whatever wallet we’re using — in this example, we’ll be using the Temple Wallet via the Chrome extension.
Get Coding
Once you’re in the root directory of our project, run npm run dev
to start the server. The server should start on localhost:3000
if you have nothing else running. Navigate to localhost:3000
. You should be presented with a page like below
Connecting to an RPC Node
We’ll need the RPC_URL
and CONTRACT_ADDRESS
constants to help us get rolling. The RPC_URL
is the URL of a server available over the internet through which we can reach a node on the Tezos blockchain. It abstracts away the complexities of setting up, running, and connecting to our own Tezos node. If you want to learn how to set up your own node, learn more here.
We’ll create a file to hold our constants. In the src
folder, create a helpers
folder and a constants.js
file. This way we’ll keep our files organized and readable. Next, add the following.
const RPC_URL = "https://ghostnet.ecadinfra.com";
const CONTRACT_ADDRESS = "KT1EYcft2Loc4ykQTzTzG796yviNbGMLJnoq";
export { RPC_URL, CONTRACT_ADDRESS };
We’ll be using the RPC_URL
above to reach a Tezos node being run by ECAD Labs — creators of the Taquito library. We’ll connect to this Node using Taquito.
Open up the index.js file in src > pages > index.js.
Add the following to your import
statements. I’ve added some comments in the code blocks to help explain what’s going on.
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { CONTRACT_ADDRESS, RPC_URL } from "../helpers/constants";
import { useEffect, useState, useRef } from "react";
Then within the Home
function, add;
// Step 2 - Initialise a Tezos instance
const Tezos = new TezosToolkit(RPC_URL);
Add the code below
// Step 3 - Set state to display content on the screen
const [players, setPlayers] = useState([]); // keeps track of players in the storage
const [reload, setReload] = useState(false); // Keeps track of our application state
const [message, setMessage] = useState(""); // Keeps track of messages to show to users within the dApp
// Step 4 - Set a wallet ref to hold the user's wallet instance later on
const walletRef = useRef(null);
Connecting the dApp to the User’s Wallet
Before a user can vote in our dApp, they’d need to connect their wallet. Let’s write a simple function that allows our app to connect to the user’s wallet.
// Step 5 - Create a ConnectWallet Function
const connectWallet = async () => {
setMessage("");
try {
const options = {
name: "Ballon tz'or",
network: { type: "ghostnet" },
};
const wallet = new BeaconWallet(options);
walletRef.current = wallet;
await wallet.requestPermissions();
Tezos.setProvider({ wallet: walletRef.current });
} catch (error) {
console.error(error);
setMessage(error.message);
}
};
Disconnect Wallet
Now, let’s have a function to disconnectWallet
// Step 6 - Create a function to allow users disconnect their wallet from the
// dApp
const disconnectWallet = () => {
setMessage("");
if (walletRef.current != "disconnected") {
walletRef.current?.client.clearActiveAccount();
walletRef.current = "disconnected";
console.log("Disconnected");
} else {
console.log("Already disconnected");
}
};
Getting the List of Players from the Storage
Next, we need to connect to the storage on the chain to get a list of players and the votes they have received.
const getPlayers = async () => {
try {
const contract = await Tezos.contract.at(CONTRACT_ADDRESS);
const storage = await contract.storage();
const players = [];
if (storage) {
storage.players.forEach((value, key) => {
players.push({
playerId: key,
name: value.name,
year: value.year,
votes: value.votes.toNumber(),
});
});
}
setPlayers(players);
return players;
} catch (error) {
setMessage(error.message);
console.log(error);
}
};
We’ll use this same function to reload the data displayed on the page each time a vote is cast.
Sending a Transaction
To vote for a player, we need our dApp to send a transaction to the chain -i.e. to interact with our Smart contract. We’ll write a votePlayer
function that sends our vote to the smart contract.
// Step 8 - Create a function that calls the increase_votes entrypoint
// and reloads the page to update it and display the new state of the storage
const votePlayer = async (playerId) => {
try {
await connectWallet();
const contract = await Tezos.wallet.at(CONTRACT_ADDRESS);
const op = await contract.methods.increase_votes(playerId).send();
setMessage("Awaiting Confirmation....");
const hash = await op.confirmation(2);
console.log(hash);
if (hash) {
setMessage("Vote Confirmed.");
setReload(true);
}
} catch (error) {w
setMessage(error.message);
console.log(error);
}
};
One thing to note from above, the line const hash = await op.confirmation(2)
tells our application to check for the confirmation of the transaction after 2 blocks. On Tezos, block finality is 2 — which means that after 2 blocks, you can say for certain that your transaction is complete.
Getting Players when Pages Load
The last function we need to write is our useEffect
that would run when our dApp loads to fetch the players from the storage.
// Step 9 - Use a useEffect to call the getPlayers function when the page loads
// initially
useEffect(() => {
getPlayers(); // run when the page loads
if (reload) {
setReload(false);
}
}, [reload]); // if reload is true (when the transaction is confirmed), run the useEffect function again
Our useEffect
function will run when the page loads to fetch the data from the storage. It’ll also run when the votePlayer
function runs and we get a hash. See the votePlayer
function for more.
Full Code
Here’s the full code for our dApp’s index.js
file including all the styles
import { Inter } from "next/font/google";
import Card from "@/components/Card";
// Step 1
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { CONTRACT_ADDRESS, RPC_URL } from "@/helpers/constants";
import { useEffect, useState, useRef } from "react";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
// Step 2 - Initialise a Tezos instance
const Tezos = new TezosToolkit(RPC_URL);
// Step 3 - Set state to display content on the screen
const [players, setPlayers] = useState([]);
const [reload, setReload] = useState(false);
const [message, setMessage] = useState("");
// Step 4 - Set a wallet ref to hold the wallet instance later
const walletRef = useRef(null);
// Step 5 - Create a ConnectWallet Function
const connectWallet = async () => {
setMessage("");
try {
const options = {
name: "Ballon tz'or",
network: { type: "ghostnet" },
};
const wallet = new BeaconWallet(options);
walletRef.current = wallet;
await wallet.requestPermissions();
Tezos.setProvider({ wallet: walletRef.current });
} catch (error) {
console.error(error);
setMessage(error.message);
}
};
// Step 6 - Create a function to allow users disconnect their wallet from the
// dApp
const disconnectWallet = () => {
setMessage("");
if (walletRef.current != "disconnected") {
walletRef.current?.client.clearActiveAccount();
walletRef.current = "disconnected";
console.log("Disconnected");
} else {
console.log("Already disconnected");
}
};
// Step 7 - Create an async function that fetches the users from the storage
const getPlayers = async () => {
try {
const contract = await Tezos.contract.at(CONTRACT_ADDRESS);
const storage = await contract.storage();
const players = [];
if (storage) {
storage.players.forEach((value, key) => {
players.push({
playerId: key,
name: value.name,
year: value.year,
votes: value.votes.toNumber(),
});
});
}
setPlayers(players);
return players;
} catch (error) {
setMessage(error.message);
console.log(error);
}
};
// Step 8 - Create a function that calls the increase_votes entrypoint
// and reloads the page to update it and display the new state of the storage
const votePlayer = async (playerId) => {
try {
await connectWallet();
const contract = await Tezos.wallet.at(CONTRACT_ADDRESS);
const op = await contract.methods.increase_votes(playerId).send();
setMessage("Awaiting Confirmation....");
const hash = await op.confirmation(2);
console.log(hash);
if (hash) {
setMessage("Vote Confirmed.");
setReload(true);
}
} catch (error) {
setMessage(error.message);
console.log(error);
}
};
// Step 9 - Use a useEffect to call the getPlayers function when the page loads
// initially
useEffect(() => {
getPlayers();
if (reload) {
setReload(false);
}
}, [reload]);
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
<div>
<h1 className="text-2xl font-bold mb-3">Ballon Tz'or 2023</h1>
</div>
<div className="flex flex-col items-center w-full gap-2">
{players?.length > 0
? players.map((player, index) => (
<Card
index={index}
onClick={() => votePlayer(player.playerId)}
playerId={player.playerId}
playerName={player.name}
votes={player.votes}
/>
))
: "Loading....."}
</div>
<p className="my-3">
{message ? <span className="text-gray-300">{message}</span> : ""}
</p>
<button
className="mt-4 rounded-full bg-red-500 p-3 hover:bg-red-700 transition-all ease-in-out"
onClick={() => disconnectWallet()}
>
Disconnect Wallet
</button>
</main>
);
}