• Time to read 5 minutes
Voting on a Blockchain: Voting DApp Code Walk-through

Voting on a Blockchain: Voting DApp Code Walk-through

This is the last of the 5 articles I wrote to explain how an end-to-end Balloting system on Ethereum might work. In this segment, I will explain the codes behind the Voting module of the DApp. 

1. Voting on a Blockchain: How it works
2. Voting on a Blockchain: Solidity Contract Codes explained
3. Voting on a Blockchain: DApp Demonstration
4. Voting on a Blockchain: Ballot Management DApp Code Walk-through
5. Voting on a Blockchain: Voting DApp Code Walk-through

In May, I was invited to give a talk about the role of Blockchain in Democratic aocieties to a group in Singapore known as the "Singapore Futurists". I decided that rather than to just talk about it, perhaps I should demonstrate how voting on the Blockchain may work. The talk eventually turned into this series of articles and a live demo that I I use frequently in my Blockchain workshops.

Recap

The Voting Module of the Ballot DApp lets voters cast their votes for the Chairman's proposal. The complete source code to this module can be found in my Github repository

Setting Up

 var web3 = new Web3();
 var accountaddress;
 var ballotContract;
 var ballotByteCode;
 var Ballot;
 var ballotABI = [{"constant":false,"inputs":[],..."voteDone","type":"event"}];
        
 $( document ).ready(function() {
     $('#panels_contract').hide();
 });

The DApp starts by setting up the global variables that will be used throughout the voting life-cycle, namely:

  1. accountaddress: this is the Ethereum account of the chairman who manages the voting process. This address is used when executing the Ballot smart contract.
  2. ballotContract: this stores the address of the Ballot Smart Contract that the voter will provide. The chairman will give the voter the Ballot Smart Contract's address after he has kick-started the voting process.
  3. Ballot: this variable stores the Ballot object as we execute and monitor it.
  4. ballotABI: (I truncated this in the code snippet above) this is the ABI for the Ballot smart contract.

When the page loads I keep panels_contract hidden because since the voter hasn't provided his wallet address at this point, the voting DApp isn't obliged to tell him what the proposal is.

 window.addEventListener('load', async () => {
     // Modern dapp browsers...
  	if (window.ethereum) {
     	    window.web3 = new Web3(ethereum);
       		try {
       		    // Request account access if needed
       		    await ethereum.enable();
       		    // Acccounts now exposed
       		    accountaddress = web3.givenProvider.selectedAddress;
                ballotContract = new web3.eth.Contract(ballotABI);        		    
      		} catch (error) {
       			// User denied account access...
       		}
       	}
       	// Legacy dapp browsers...
       	else if (window.web3) {
       		    window.web3 = new Web3(web3.currentProvider);
       		    // Acccounts always exposed
       		    web3.eth.sendTransaction({/* ... */});
       	}
       	// Non-dapp browsers...
       	else {
       			console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
       	}
});

When the page loads, this code checks if the browser comes with an Ethereum Wallet such as MetaMask. Once found, we load the wallet's active address into the accountaddress variable. We also load the Ballot Contract's ABI into the ballotContract variable. 

If your user is running this DApp in a browser that doesn't have an Ethereum wallet plug-in, it pops a message in your browser's console to tell you that it can't run the DApp. In a live implementation you probably want to redirect these users to a page to download a digital wallet.

Showing the Proposal

 function getContract(_ballotContractAddress){
     Ballot = new web3.eth.Contract(ballotABI, _ballotContractAddress);
     Ballot.methods.ballotOfficialName().call().then((result) => {
         $("#lbl_officialname").html("<b>Ballot Official Name: </b>" + result);
     });
     Ballot.methods.proposal().call().then((result) => {
         $("#lbl_proposal").html("<b>Proposal: </b>" + result);
     });
            
     loadFinalResult(Ballot);
     loadState(Ballot);
            
     watchVoteStarted(); //start watching for events
     watchVoteEnd(); //start watching for vote end
     watchVoteDone(); //start watching for vote done
            
     $('#panels_contract').show();
     $("#loader").hide();
     $("#section_voting").hide();
}

The getContract function gets called when the voters provides the Voting contract address and presses the [Go] button. Here, the Ballot contract is loaded and the Ballot Official Name and Proposal details are displayed.

It executes loadFinalResult() to display the voting result (if voting has completed). It also executes loadState() to display the current state of the Voting process - whether it is in the Created, Voting or Ended state.

The watchVoteStarted(), watchVoteEnd() and watchVoteDone() functions will wait and watch for the Voting Smart Contract to trigger these events. These events get triggered at various points during the voting process.

Watching...Watching

 function watchVoteEnd(){
     Ballot.events.voteEnded({
     }, (error, event) => { 
         console.log(event.returnValues.finalResult);
         loadState(Ballot);
         loadFinalResult(Ballot);
     })
     .on('data', (event) => {
     })
     .on('changed', (event) => {
     })
     .on('error', console.error)                      
 }

watchVoteEnd() waits for the Voting Smart Contract to trigger the voteEnded event. When this happens, it means that the Chairman has ended the voting process. loadState() executes to display a message in the user interface to tell the voter that voting has ended. loadFinalResult() executes to show the voter the results.

 function watchVoteStarted(){
     Ballot.events.voteStarted({
     }, (error, event) => { })
     .on('data', (event) => {
         console.log(event.event); 
         loadState(Ballot);
     })
     .on('changed', (event) => {
     })
     .on('error', console.error)
 }

watchVoteStarted() waits for the Voting Smart Contract to trigger the voteStarted event. When this happens, it means that the Chairman has started the voting process. loadState() executes to display a message in the user interface to tell the voter that voting has started.

function watchVoteDone(){
    Ballot.events.voteDone({
    }, (error, event) => { 
        var strEventAddress = event.returnValues.voter.toString().toLowerCase();
        var strAccountAddress = accountaddress.toString().toLowerCase();
                
        console.log(strEventAddress);
        console.log(strAccountAddress);
        console.log(strEventAddress.valueOf() == strAccountAddress.valueOf())
                
        if (strEventAddress.valueOf() == strAccountAddress.valueOf()) {
            $("#loaderChoice").hide();
            loadState(Ballot);
         }
    })
    .on('data', (event) => {
    })
    .on('changed', (event) => {
    })
    .on('error', console.error)           
}

watchVoteDone() waits for the voteDone event to be triggered. voteDone is triggered when the voter votes. Since voteDone is triggered every time any of the voters in the voter register votes, the code checks if this voteDone event is triggered by this voter himself, rather than another voter who happens to also be concurrently voting. It then reloads the state of the Ballot with loadState() to hide the [Yes] and [No] button.

Loading Ballot Information

async function loadFinalResult(myBallot){
    myBallot.methods.finalResult().call().then((result) => {
        $("#lbl_result").html("<b>Result: </b>" + result);
    });
}

loadFinalResult() picks up the final voting result from the Ballot Smart Contract and displays it in the Voting DApp's User Interface.

async function loadState(myBallot){
    myBallot.methods.state().call().then((result) => {
        if (result == 0){
            $("#lbl_state").addClass("label label-primary");
            $("#lbl_state").html("Created"); 
        }
        else if (result == 1){
            $("#lbl_state").addClass("label label-success");
            $("#lbl_state").html("Voting"); 
        }                
        else if (result == 2){
            $("#lbl_state").addClass("label label-danger");
            $("#lbl_state").html("Ended");                    
        } 
        loadVoter(myBallot, accountaddress, result);
        return result;
    });
}

loadState() finds out the current state of the Ballot Smart Contract and displays either "Created", "Voting" or "Ended" in the Voting DApp's User Interface. 

async function loadVoter(myBallot, _myVoterAddress, _state){
    myBallot.methods.voterRegister(_myVoterAddress).call().then((result) => {
        if (result.voterName != "" && _state == 1) {
            $("#loaderChoice").hide();
            $("#section_voting").show();
                    
            $("#lbl_voter").html("<b>Your Name: </b>" + result.voterName);
            console.log(result.voted);
            if (result.voted){
                $("#btnYes").hide();
                $("#btnNo").hide(); 
                $("#lblVoted").html("<span class='label label-primary'>Voted</span>");
            }
            else {
                $("#btnYes").show();
                $("#btnNo").show();                        
            }
        }
        else{
            $("#btnYes").hide();
            $("#btnNo").hide();                     
        }
    } );
}

loadVoter() checks the Voter Register on Ballot Smart Contract to see if this voter's wallet address is registered as a voter. If he isn't, then he isn't eligible to vote. The [Yes] and [No] buttons are hidden. 

It also checks if the current state of the Voting Smart Contract allows voting. If it does, then the [Yes] and [No] buttons are turned on for the voter to press. 

Finally, it checks if the voter has already voted previously. If he has, the [Yes] and [No] buttons are hidden, and the vote sees only the "Voted" notice.

The Buttons

$("#btnYes").click(async function(){
    $("#loaderChoice").show();
            
    //estimate first
    var mygas;
   	Ballot.methods.doVote(true).estimateGas({from: accountaddress})
    .then(function(gasAmount){
    mygas = gasAmount;
    })
            
   	Ballot.methods.doVote(true).send({
        from: accountaddress,
        gas: mygas, 
        gasPrice: web3.eth.gasPrice      	    
   	})
    .on('transactionHash', (hash) => {

    })
    .on('receipt', (receipt) => {
        console.log("done");
    })
    .on('confirmation', (confirmationNumber, receipt) => {

     })
    .on('error', console.error);
});

$("#btnNo").click(async function(){
    $("#loaderChoice").show();
    //estimate first
    var mygas;
   	Ballot.methods.doVote(false).estimateGas({from: accountaddress})
    .then(function(gasAmount){
        mygas = gasAmount;
    })
            
  	Ballot.methods.doVote(false).send({
        from: accountaddress,
        gas: mygas, 
        gasPrice: web3.eth.gasPrice       	    
   	})
    .on('transactionHash', (hash) => {

     })
    .on('receipt', (receipt) => {
        console.log("done");
    })
    .on('confirmation', (confirmationNumber, receipt) => {

    })
    .on('error', console.error);
});  

The [Yes] and [No] button will execute Ballot.methods.doVote(true) and Ballot.methods.doVote(false) on the Ballot Smart Contract to vote on the proposal.

$("#btnGo").click(async function() {	
    $("#loader").show();
    var _ballotAddress = $("#txtBallotAddress").val();
    getContract(_ballotAddress);
});

The [Go] button is pressed by the user to load the Ballot Smart Contract. It executes getContract() to let the voter see the proposal and vote it.

Retrospect

This completes Voting on a Blockchain. I have thoroughly enjoyed developing the Voting DApp along with this series of articles that came with it. Do send me your feedback. Let me know how it can be better, if you spotted a bug or if you have forked my codes and built something cool out of it.

If you enjoyed this tutorial, perhaps you may also wish to read:

Photo by Michael on Unsplash