A-Z of building dApps on ICE — Part 3: Voting dApp

ICONOsphere
10 min readMar 8, 2022

--

This is the part 3 of the A-Z of building dApps on ICE series. At this point you should have a good idea on setting up the environment, required tools, and deploying smart contracts on the Arctic testnet. Please go through Part 1 and Part 2, if you have not already.

What we will learn

In this article, we will learn about building a decentralized voting application using:

  • Solidity to write smart contracts
  • Hardhat to deploy smart contracts
  • React.js to create a web UI

Prerequisite

  • Node.js >= v14.17.3 (NVM)
  • Metamask browser extension configured with Arctic testnet (Configuration)
  • An account with funds (Faucet)

Introduction

We will be using Hardhat to deploy a voting smart contract written in Solidity, and we will interact with the contract using a web application built using reactJS and web3.js.

The voting dApp we will be working on is simple voting app, where registered voters cast their vote to for a candidate of their choice. At the end of the election, the candidate with the maximum number of votes is announced as the winner. Each voter can cast their vote to a single candidate in the election. The contract owner is responsible for adding voters, candidates and ending the voting process.

Each voter involved in the voting process is represented by an EOA (Externally Owned Account).

By the end of this tutorial we should have a functional voting dApp on the Arctic testnet.

Voting smart contract

Voting smart contract

As we see in our contract code, we have named our voting contract MyBallot. The MyBallot contract has two structs defined — Candidate and Voter. Structs in simple terms, are collections of same or different data types, under a single name. In our smart contract, Candidate represents an election candidate with a unique identifier, name and the number of votes they have received. Voter represents a voter in the election with a wallet address and a voted flag to determine if the voter has already voted for a candidate.

It is interesting to note that in our contract, candidates don’t have a wallet addresses associated with them, but voters have. That’s because our contract is designed in such a way that the contract owner can create those candidates arbitrarily. However, the voters are required to cast their vote by creating a transaction in the blockchain, so they should have an active wallet address associated with them.

The contract owner adds candidates and voters by calling the addCandidate() and addVoter() functions respectively. Then the registered voters cast their vote to those candidates by invoking the vote() function. The voting process is allowed to run for sometime and is ended by the contract owner by calling the endVote() function. This function sets the isVoteEnded flag to true which indicates the end of election . We can get the winner of the election by querying the winningCandidateId which returns the id of candidate with the highest vote count.

Deploying to arctic testnet

Create Project Folder

Create a folder to store our contract. Then initialize the project with npm and install required packages:

npm init -y
npm install --save-dev hardhat
npm install ethers @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers dotenv

Run hardhat

In the same directory, run:

npx hardhat

Select Create an empty hardhat.config.js with your keyboard and hit enter.

Create empty hardhat.config.js

Store wallet Private Key

In the project’s root path, please create a .env file & open that file.

touch .env

In .env file, please add a variable called PRIVATE_KEY and assign the exported private key to it.

PRIVATE_KEY = <paste_your_private_key_here>

NOTE: You can get the private key for your metamask wallet by following instructions from here. Also Remove <> while pasting your_private_key.

Configure hardhat.config.js file

In the empty hardhat.config.js file, add the following configurations:

require('dotenv').config();
require("@nomiclabs/hardhat-waffle");
const ICE_PRIVATE_KEY = process.env.PRIVATE_KEY;
module.exports = {
solidity: "0.8.0",
networks: {
testnet: {
url: `https://arctic-rpc.icenetwork.io:9933`,
accounts: [`0x${ICE_PRIVATE_KEY}`]
}
}
};

Create contract and deploy scripts

Create a contracts folder in the project’s root path, and then create Voting.sol in it.

mkdir contracts
cd contracts
touch Voting.sol

Put the MyBallot contract code in Voting.sol file.

Then create another folder scripts in root and then create a deploy.js file inside scripts, where we will write deployment scripts.

mkdir scripts
cd scripts
touch deploy.js

Add the following code to the empty deploy.js file.

async function main() {
const [deployer] = await ethers.getSigners();

console.log("Deploying contracts with the account:", deployer.address);

console.log("Account balance:", (await deployer.getBalance()).toString());

const Voting = await ethers.getContractFactory("MyBallot");
const voting = await Voting.deploy();

console.log("Contract address:", voting.address);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Your project folder should be similar to this:

Project folder structure

Compile and deploy

First compile your contract to generate the contract ABI. Application Binary Interface (ABI) is required when interacting with a deployed contract. In simple terms, it is a list of a contract’s functions and arguments in JSON format.

Run the following command in the project’s root folder:

npx hardhat compile

It will create MyBallot.json file inside a new artifacts folder. This file contains the contract’s ABI.

Now our contract can be deployed with the following command.

npx hardhat run scripts/deploy.js --network testnet

You should see the deployed contract address in the terminal.

contract address in terminal

Please keep track of this contract address as this will be required by our app frontend.

You can find more about deployment on Arctic testnet network here.

If you get confused at any point, you can refer to this code at Github for reference.

Now that we have deployed our smart contract to the Arctic testnet ,we will next work on building the UI and interacting with the MyBallot contract.

Building the Frontend

You can find the frontend code in the voting-dapp/client folder in this github repo.

The frontend directory has the following structure and contents:

client/
+---public/
| favicon.ico
| index.html
| logo192.png
| logo512.png
| manifest.json
| robots.txt
+---src/
| App.css
| App.js
| App.test.js
| index.js
| reportWebVitals.js
| setupTests.js
+-------components/
| CandidateList.js
| Header.js
| Loader.js
| VotingForm.js
+-------contractAbi/
| abi.js
+---.env
+---.gitignore
+--- config-overrides.js
+--- package-lock.json
+--- package.json
+--- README.md

The frontend is created using create-react-app. The main logic is written in the App.js file. For styling, material ui library and styled-components are used. The web3.js library is chosen to communicate with the Arctic testnet.

Running the Frontend

To get started quickly, you can clone the repo, and deploy the MyBallot contract (in case you haven’t done it already). Create a .env file inside the client folder and copy/paste your contract address in it like:

REACT_APP_CONTRACT_ADDRESS = <contract address>.

Note: Remove <> while pasting contract address

Also replace the value of myBallot variable in client/src/contractAbi/abi.js with the contents of artifacts/contracts/MyBallot.json.

Now that the frontend is configured, you can switch to the client folder and run the app in your browser by using the command:

$ cd client
$ npm install
$ npm start

The Voting dApp frontend will launch and will be accessible via any web browser at http://localhost:3000.

Note: Make sure you have configured your metamask with the Arctic testnet before building the UI since we have deployed our contract there. You can make the configuration following the instructions here.

Taking a look at the UI

Voting dApp UI

Since UI/UX is not the main focus of this article, we have kept the UI simple.

The UI has three sections — Header Section, Admin Section and Voting Details. The Header Section shows the election status and the wallet address of the currently logged-in voter.

The Admin Section is solely for the contract owner. They can add candidates and voters from this section, and after certain time, end the voting session which will update the winning candidate’s details on the UI.

The Voting Details Section lists the candidates, and users can vote for one of those candidates by clicking on the vote button. At the bottom, it displays Voter Status: registered if the currently logged in candidate is allowed to vote on this election. Finally, when the election process comes to an end, the details of the winner of the election will be shown.

Understanding the code

Now let’s go through understanding important sections of the frontend code. The main logic is inside the src/App.js file. All the UI components are inside src/components folder.

In the src/App.js file, you will find the following two snippets.

// Snippet 1
async function init() {
if(window.ethereum) {
window.web3 = new Web3(window.ethereum);
login();
} else {
console.log('Metamask not installed!')
}
}
// Snippet 2
cosnt login = async () => {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setAccount(accounts[0]);
}

Here, we first check if window.ethereumexists, then we create a window.web3 object by passing window.ethereum which will set the provider to the current network on which metamask is selected. In our case, it should be the arctic testnet network.

The await window.ethereum.request({method: 'eth_requestAccounts'}) function calls the pop-up UI dialogue that asks the user’s permission to connect the dApp to MetaMask and gets the user’s address.

Now, we will create an instance of our voting contract, that will be used to interact with the smart contract methods.

const voting = new window.web3.eth.Contract(myBallot.abi, contractAddress);

Here, an instance of the voting contract is created by passing the abi and contractAddress of our smart contract. We can use this instance to call functions on the MyBallot contract.

For example, you can get the contract owner using the following snippet:

const getOwner = await voting.methods.owner().call();

Here, owner() is the read only function which return the contract owner which can be achieved by call() function.

Similarly, the following code is used to add a candidate:

// Code snippet from src/App.js fileconst addCandidateTx = await votingContract.methods.addCandidate(cName).send({ from: account});
console.log(addCandidateTx.transactionHash)

Here, we are invoking the write function addCandidate() , which can only be done by the contract owner. Since it is a state changing function, we need to call send({ from: account }), which invokes the metamask wallet and asks for permission to confirm the transaction.

Adding Candidates

Now that we have an idea on how to make both read and write calls to smart contracts, we can easily invoke all the necessary smart contract methods to complete all the functionalities of the voting dApp.

Let us now look at how we can retrieve the list of added candidates which is displayed in the Candidate List table in UI.

// Code snippet from src/App.js fileconst getCandidates = async () => {
const candidatesCount = await voting.methods.candidatesCount().call();
const candidatesId = await voting.methods.getCandidatesId().call(); let candidates = []; for (let i = 0; i < candidatesCount; i++) {
const candidate = await voting.methods.candidates(candidatesId[i]).call();
candidates.push({
id: candidate.id,
name: candidate.name,
voteCount: candidate.voteCount
})
} ...
}

The function candidates(candidateId) returns an object containing candidate’s id, name and voteCount of a single candidate. It is not a function that we defined explicitly on the MyBallot contract, but is accessible here since we made the candidates variable public on the contract.

Similarly, candidatesCount returns the number of candidates added and candidatesId returns the list of ids of the all candidates. To get the list of all the candidates, we first get ids of all the candidates by calling the getCandidates function, and then we fetch the details for each of those ids.

Now, let us look at how to call the vote function, which will be used to cast the vote.

// Code snippet from src/App.js file
const voteTx = await votingContract.methods.vote(id).send({ from: account });

Using the snippet above, a registered voter (account) can cast the vote by calling the vote().send() method. We will be passing the candidate’s id as the parameter in the vote function and since it is state changing function we be using send({ from: account }) which will send the transaction from logged in account, asking user to confirm it.

Casting Vote

Finally, the contract owner can end the vote by invoking the endVote function. Also along with ending the voting process , we also store the winner’s details in the local storage to keep track of winners of each election.

// Code snippet from src/App.js fileconst winningCandidateId = await votingContract.methods.winningCandidateId().call();const winner = candidateDetails.find(item => item.id ==  winningCandidateId);const endVoteTx = await votingContract.methods.endVote().send({ from: account });if(endVoteTx.transactionHash) {    setWinningCandidate(winner)
}

Here we can get the winning candidate’s id by invoking winningCandidateId() function. After getting the winningCandidateId, we can easily find their details through the candidateDetails variable which stores all the candidates of the election, using JavaScript array’s find method. The contract owner can end the vote by invoking the endVote function which sets isVoteEnded flag to true. This formally ends the election and the winner’s information is then shown in the Voting Details section.

Ending vote and declaring winner

Limitations and further development

  1. This tutorial is for learning purpose and the code/logic used here should not be considered for production-ready dApps.
  2. In the current MyBallot contract, once the voting is ended, the contract is practically useless. This can be improved by starting a new ballot after the previous one has ended, and tracking candidates, voters and voting information for each of the ballots separately.

Conclusion

In this final part of the development on ICE series, we deployed the MyBallot smart contract to the Arctic network using hardhat and then developed UI using ReactJS and web3.js to communicate with the deployed contract.

We hope the three part series of A-Z of building dApps on ICE has been of service for development on ICE/Snow blockchain(Arctic testnet). Please leave a comment if you have any issues while following the tutorials.

Thank you :)

--

--

ICONOsphere
ICONOsphere

Written by ICONOsphere

With a deep history of commitment to building projects that foster community growth & personal empowerment we work to create a nation built on trust with icon.

No responses yet