• Time to read 8 minutes
Voting on a Blockchain: Ballot Management DApp Code Walk-through

Voting on a Blockchain: Ballot Management DApp Code Walk-through

This is the 4th of 5 articles to explore the development of an end-to-end Balloting system on Ethereum. In this article, I will explain the codes behind the Ballot Management module of the DApp. 

Do check back often as I work on the rest of the articles in this series.

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

Recap

The Ballot Management Module of the Ballot DApp lets the chairman set the proposal, setup the voting register, open/close the ballot and monitor voters as they vote. It also releases the voting results. The complete source code to this module can be found on my Github repository

Setting Up

        var web3 = new Web3();
        var accountaddress;
        var ballotContract;
        var ballotByteCode;
        var Ballot;
        var ballotABI = [{"constant":false,"inputs":[]....."type":"event"}];
        var voterTable;
        
        $( document ).ready(function() {
            $('#kaleidorefresh').hide();
            $('#panels_contract').hide();
            $('#panels_voters').hide();
       	    voterTable = $('#voterTable').DataTable( {
                columns: [
                    { title: "Address" },
                    { title: "Name" },
                    { title: "Status" }
                ]
            } );
        });

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 usedt when executing the Ballot smart contract.
  2. ballotContract: this stores the address of the ballot smart contract that will be generated once the ballot smart contract is deployed.
  3. ballotByteCode: here's where the byte code of the ballot smart contract is ekpt. We will populate the byte code in this variable later on.
  4. Ballot: this variable stores the Ballot object as we execute and monitor it.
  5. ballotABI: (I truncated this in the code snippet above) this is the ABI for the Ballot smart contract.
  6. voterTable: this will store a datatable where we keep track of the voters, their addresses, and their voting status.

When the page loads, we do the following:

  1. Hide the panel that displays the Ballot contract info because a Ballot contract has yet to be deployed when the page just loads.
  2. Hide the panel that displays voters at this stage as well. There aren't any voters yet.
  3. I also load the datatable that displays voters' info in 3 columns, namely, address (which contains the voters' ethereum addresses), names (which is their real name, Zoe Ellie etc.) and statuses (whether they have already voted or not).
        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);
                    ballotByteCode = '0x608060....4052600080';
        		    
        		} 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!');
        	}
        });
        var BallotContractAddress = "";
        var MyTransactionHash;

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, and the Ballot Contract's byte code into the ballotByteCode variable. The Ballot's byte code is obtained when you compile the ballot smart contract. 

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

Watching & Waiting

In the DApp's watching section, I watch for various events that the Ballot smart contract will trigger at various points during the voting life-cycle.

        function watchVoteEnd(){
            Ballot.events.voteEnded({
            }, (error, event) => { 
                console.log(event.returnValues.finalResult);
                loadState(Ballot);
                loadFinalResult(Ballot);
                $("#loaderStartVote").hide();
                $("#btnEnd").hide();
            })
            .on('data', (event) => {
            })
            .on('changed', (event) => {
                // remove event from local database
            })
            .on('error', console.error)                      
        }

In the watchVoteEnd() function, I watch and wait for the Ballot smart contract to trigger a voteEnded event. This signifies that voting has ended and it is now time to do the following:

  1. Execute loadState() to change the current state of the Ballot to "Ended".
  2. Execute loadFinalResult() to display the voting result.
  3. Hide the spinning loader animated GIF image since voting has ended and we aren't waiting for anything to load in the background anymore.
  4. Hide the [End Vote] button since voting has ended, so the chairman shouldn't be allow to re-attempt to end voting again.
        function watchVoteDone(){
            Ballot.events.voteDone({
            }, (error, event) => { 
                console.log(event.returnValues.voter);
                updateNewVote(event.returnValues.voter);    
            })
            .on('data', (event) => {
            })
            .on('changed', (event) => {
                // remove event from local database
            })
            .on('error', console.error)           
        }

In the watchVoteDone() function, a voter has just voted. So we execute updateNewVote() to update the voter table to change his status from "Not Voted" to "Voted".

        var lastVoteAdded="";
        function watchVoterAdded(){
            Ballot.events.voterAdded({
            }, (error, event) => { 
                console.log(event.returnValues.voter);
                loadTotalVoter(Ballot);
                
                //strange hack: this event fires twice for some reasons
                //so I save the last voter address and suppress it if
                //it is the same as the previous one :P
                if (lastVoteAdded != event.returnValues.voter){
                    loadVoter(Ballot, event.returnValues.voter);
                    lastVoteAdded = event.returnValues.voter;                    
                }
                $("#loaderNewVoter").hide();                
            })
            .on('data', (event) => {
            })
            .on('changed', (event) => {
                // remove event from local database
            })
            .on('error', console.error)
        }

In the watchVoterAdded() function, we watch and wait for a new voter to be added to the voter register. Once he is added, we stop spinning the loader GIF image and then execute loadTotalVoter() to update the total number of voters displayed on the screen.

        function watchVoteStarted(){
            Ballot.events.voteStarted({
            }, (error, event) => { })
            .on('data', (event) => {
                console.log(event.event); // same results as the optional callback above
                $("#loaderStartVote").hide();
                $("#btnStart").hide();
                $("#btnEnd").show();
                $("#section_addVoter").hide();
                loadState(Ballot);
            })
            .on('changed', (event) => {
                // remove event from local database
            })
            .on('error', console.error)
        }

In the watchVoteStarted() function, we watch for the event triggered by the Ballot smart contract to signal that voting has begun. This happens when the chairman presses [Start Vote]. When this happens, we:

  1. Stop the loaderStartVote spinning GIF image from spinning since voting has already been started.
  2. We hide the [Start Vote] button since you can't restart a voting process that you have already started once.
  3. We show the [End Vote] button since it is now possible to end a voting process that has been started.
  4. We hide the [Add Voter] button since it is no longer possible to add voters to a voting process that has already started.

Updating the DApp's User Interface

The functions in this section updates the various parts of the Ballot Management DApp's user interface by reading the corresponding values from the Ballot smart contract and displaying them there.

        async function loadBallotContract(myBallotContractAddress){
        	Ballot = new web3.eth.Contract(ballotABI, myBallotContractAddress);
        	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);
            loadTotalVoter(Ballot);
            loadTotalVotes(Ballot);
            loadState(Ballot);
            
            $("#lbl_address").html("<b>Address: </b>" + myBallotContractAddress);           
        };
        

The loadBallotContract() function reads the name of the chairman, proposal and the address of the Ballot smart contract from the Blockchain and displays them on the corresponding sections of the DApp's UI. It also loads the final result of the voting process, the total number of voters, total number of votes and the current state of the voting process from the Ballot smart contract.

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

The loadFinalResult() function reads the voting results from the Ballot Smart Contract on the Blockchain and displays it in the corresponding section of the DApp's UI.

        async function loadTotalVoter(myBallot){
            myBallot.methods.totalVoter().call().then((result) => {
                $("#lbl_voters_num").html("<b>Voters: </b>" + result);
            });
        }

The loadTotalVoter() function reads the value of the total number of voters from the Ballot Smart Contract on the Blockchain and displays it in the corresponding section of the DApp's UI.

        async function loadTotalVotes(myBallot){
            myBallot.methods.totalVote().call().then((result) => {
                $("#lbl_votes_num").html("<b>Votes: </b>" + result);
            });   
        }

The loadTotalVotes() function reads the value of the total number of votes from the Ballot Smart Contract on the Blockchain and displays it in the corresponding section of the DApp's UI.

        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");                    
                } 
                return result;
            });
        }

The loadState() function reads the current state of the Ballot Smart Contract on the Blockchain and displays it in the corresponding section of the DApp's UI. The voting process has 3 state:

  1. Created: when the voting proposal has been set and the ballot contract has just been initialized.
  2. Voting: When voting is ongoing
  3. Ended: When voting has ended
        async function loadVoter(myBallot, _myVoterAddress){
            myBallot.methods.voterRegister(_myVoterAddress).call().then((result) => {
                console.log(result);
                
                var voteStatus;
                if (result.voted){
                    voteStatus = "<span class='label label-primary'>Voted</span>";
                }
                else {
                    voteStatus = "<span class='label label-danger'>Not Voted</span>";
                }
                
                var newRow = voterTable.row.add( [
                    _myVoterAddress,
                    result.voterName,
                    voteStatus
                ] ).draw(false).node();
                $('td:eq(2)', newRow).attr('id', _myVoterAddress+"_cell");
                
            } );
        }

The loadVoter() function reads the details of a voter from the Ballot Smart Contract on the Blockchain. It checks if this voter has voted. If he has, it changes his stated from "Not Voted" to "Voted". It also check if this voter is new. If he is, he is added to the voting register table.

        function updateNewVote(_myVoterAddress){
            $("#" + _myVoterAddress+"_cell").html("<span class='label label-primary'>Voted</span>");  
            loadTotalVotes(Ballot);
        }

The updateNewVote() function reads the voting status of a voter from the Ballot Smart Contract on the Blockchain and change his status to "Voted" in the corresponding section of the DApp's UI.

The Buttons

        $("#btnEnd").click(async function(){
            $("#loaderStartVote").show();
            //Ballot = new web3.eth.Contract(ballotABI, BallotContractAddress);
            
            var mygas;
            Ballot.methods.endVote().estimateGas({from: accountaddress})
            .then(function(gasAmount){
                mygas = gasAmount;
            })
            
        	Ballot.methods.endVote().send({
                from: accountaddress,
                gas: mygas, 
                gasPrice: web3.eth.gasPrice        	    
        	})
            .on('transactionHash', (hash) => {
                console.log("a");
            })
            .on('receipt', (receipt) => {
                console.log("b");            
                
            })
            .on('confirmation', (confirmationNumber, receipt) => {
                console.log("c");
            })
            .on('error', console.error);            
        });

The [End] button is pressed when the Chairman of the voting process is ready to end voting. It triggers the endVote() function on the Ballot smart contract in the Blockchain to execute.

        $("#btnStart").click(async function() {	
            $("#loaderStartVote").show();
            //Ballot = new web3.eth.Contract(ballotABI, BallotContractAddress);
            
            var mygas;
            Ballot.methods.startVote().estimateGas({from: accountaddress})
            .then(function(gasAmount){
                mygas = gasAmount;
            })
            
        	Ballot.methods.startVote().send({
                from: accountaddress,
                gas: mygas, 
                gasPrice: web3.eth.gasPrice        	    
        	})
            .on('transactionHash', (hash) => {
                console.log("a");
            })
            .on('receipt', (receipt) => {
                console.log("b");            
                
            })
            .on('confirmation', (confirmationNumber, receipt) => {
                console.log("c");
            })
            .on('error', console.error);
        });

The [Start] button is pressed when the Chairman of the voting process is ready to start voting. It triggers the startVote() function on the Ballot smart contract in the Blockchain to execute.

        $("#btnAdd").click(async function() {	
            $("#loaderNewVoter").show();
            console.log($("#txtNewVoterAddress").val());
            console.log($("#txtNewVoterName").val());
            
            //Ballot = new web3.eth.Contract(ballotABI, BallotContractAddress);
            
            //estimate first
            var mygas;
            Ballot.methods.addVoter($("#txtNewVoterAddress").val(), $("#txtNewVoterName").val()).estimateGas({from: accountaddress})
            .then(function(gasAmount){
                mygas = gasAmount;
            })
            
        	Ballot.methods.addVoter($("#txtNewVoterAddress").val(), $("#txtNewVoterName").val()).send({
                from: accountaddress,
                gas: mygas, 
                gasPrice: web3.eth.gasPrice       	    
        	})
            .on('transactionHash', (hash) => {
                console.log("a");
            })
            .on('receipt', (receipt) => {
                console.log("b");            
                
            })
            .on('confirmation', (confirmationNumber, receipt) => {
                console.log("c");
            })
            .on('error', console.error);
            
        });

The [Add Voter] button is pressed when the Chairman of the voting process adds a new voter to the vote register. It triggers the addVoter() function on the Ballot smart contract in the Blockchain to execute.

        $("#btnGo").click(async function() {	
            $("#loader").show();
            var i = 0;
            var _ballotOfficialName = $("#official").val();
            var _proposal = $("#proposal").val();
            
            ballotContract.deploy({
                data: ballotByteCode,
                arguments: [_ballotOfficialName, _proposal],
            })
            .send({
                from: accountaddress,
                gas: 1308700, 
                gasPrice: web3.eth.gasPrice,
                gasLimit: 2000000
            }, (error, transactionHash) => {
                MyTransactionHash = transactionHash;
                //getContract(); for private kaleido chain only
            })
            .on('error', (error) => { 
                console.log("b");            
            })
            .on('transactionHash', (transactionHash) => { 
                console.log("c");
            })
            .on('receipt', (receipt) => {
                console.log("DONE" + receipt.contractAddress); // contains the new contract address
                
                BallotContractAddress = receipt.contractAddress;
                loadBallotContract(BallotContractAddress);
                console.log(BallotContractAddress);
                $("#contractAddress").val(BallotContractAddress);
                watchVoteStarted(); //start watching for events
                watchVoterAdded(); //start watching for new voters
                watchVoteDone(); //start watching for vote done
                watchVoteEnd(); //start watching for vote end
                $('#panels_contract').show();
                $('#panels_voters').show();
                $("#btnStart").show();
                $("#btnEnd").hide();
                $("#loader").hide();
                $("#section_addVoter").show();
            })
            .on('confirmation', (confirmationNumber, receipt) => { 
                console.log(i);
                i++;
            })
            .then((newContractInstance) => {
                console.log(newContractInstance.options.address) // instance with the new contract address
            });
                        
        });

The [Go] button is pressed when the Chairman starts a new voting process. It triggers a new Ballot smart contract to be deployed on the blockchain.

Upon successful deployment, it populates the BallotContracAddress field with the address of the new Ballot smart contract that has just been deployed on the Blockchain. It executes loadBalloContract() to update the Ballot Official Name and Proposal on the DApp's UI. It then watches for new events that the Ballot contract triggers as it voting begins. 

It makes the contract panel visible so that the user can view the contract details. It makes the voters panel visible so that the chairman can add new voters. It also shows the [Start Vote] button so that the chairman can press it to declare that voting has begun. 

It disables the [End Vote] button because it is not possible to end the voting process without first starting it. 

It also shows the section that displays voters in the vote register so that the chairman may start adding voters to the Ballot smart contract.

What's Next

In the 5th (and final) part of Voting on a Blockchain, we will examine the codes for the Voter DApp module. Stay tuned.

Photo by Jesse Collins on Unsplash