Build a voting application where you will initialize a set of candidates who will be contesting in the election and then vote for the candidates. The votes will be stored on the blockchain. You will go through the process of implementing the voting contract, deploying to the local test blockchain, and interacting with the contract methods.
- NodeJS v13.5.0
- Ganache CLI v6.9.1
- Truffle v5.1.17
- Solc v0.5.8
- Windows Build Tools
npm install --global --production windows-build-tools
- Create a new folder named “Voting System”.
- Open your command prompt and go in the folder. We will now refer to this as your workspace.
- In your workspace, initialize a truffle and node project:
$ truffle init
$ npm init
- Install solc and web3 on your workspace.
$ npm install solc@0.5.8
$ npm install web3@1.2.6
- Create a new contract called Voting.sol and put it in the
contracts/
folder.
a. Implement a public
field, which will keep track of each candidate’s votes.
b. Implement a public array
to store every candidate.
c. Implement a addCandidate(string)
function, which adds the candidate to an array.
d. Implement a validCandidate(string)
function, which checks whether the given candidate is contained in the array.
e. Implement a voteForCandidate(string)
function, which votes for a given candidate.
f. Implement a totalVotesFor(string)
function, which returns the count of votes for a candidate.
g. To be able to compare strings we can first hash them with keccak256()
and compare their hashes.
- In the migrations folder create file 2_deploy_contracts.js and add the deployment code which will deploy the smart contract.
- Install
ganache-cli
: Remember, you may need administrator permissions if errors occur.
$ npm install –g ganache-cli@6.9.1
- Run
ganache-cli
: Do not close this terminal. Let it run in the background as we continue with other tasks.
$ ganache-cli
- On a new terminal in your workspace, run
node
:
$ node
- Require the
web3
library:
> Web3 = require('web3')
- Initialize the provider:
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"))
Tip: If the console gets messy due to long outputs, press CTRL + L
to clear the console.
- Retrieve and store all our available accounts. We can do that with
web3.eth.getAccounts()
. This returns a promise, so what we are going to do is to store the data in anaccounts
array for easy access.
> web3.eth.getAccounts().then(web3Accounts => {accounts = web3Accounts})
> accounts
- Read the contract and store it as a variable so we can have an easy access for later use:
> code = fs.readFileSync('contracts/Voting.sol').toString()
- Require solidity compiler (solc):
> solc = require('solc')
- Compile the code. At this point, you would already have the
bytecode
,metadata
,interface
, and so on:
> compiledCode = solc.compile(code)
If you run into solc
compiler issues, copy the code below (otherwise, disregard and continue):
> var solcInput = {
language: "Solidity",
sources: {
contract: {
content: code
}
},
settings: {
optimizer: {
enabled: true
},
evmVersion: "byzantium",
outputSelection: {
"*": {
"": ["legacyAST", "ast"],
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap",
"evm.gasEstimates"
]
}
}
}
};
> solcInput = JSON.stringify(solcInput);
> compiledCode = solc.compile(solcInput);
If you’re curious on what each property does, see the docs here: https://solidity.readthedocs.io/en/v0.5.0/using-the-compiler.html#compiler-input-and-output-json-description
- Retrieve the
abiDefinition
in JSON format by calling the command:
> abiDefinition = JSON.parse(compiledCode)['contracts']['contract']['Voting']['abi']
- Create the instance of the contract.
Notice that we first pass the ABI definition
then we pass an object where we specify the account that we want to deploy from and the gas limit
. In our case, we have a previous accounts
array with all available accounts and we use the one at index 0
(the first account).
> VotingContract = new web3.eth.Contract(abiDefinition, {from: accounts[0], gas: 4700000})
- Lastly, we need the byteCode of the contract. Use this code to retrieve it:
> byteCode = JSON.parse(compiledCode)['contracts']['contract']['Voting']['evm']['bytecode']['object']
- Now we will
deploy
the contract and store the instance of the contract incontractInstance
:
> VotingContract.deploy({data: byteCode}).send({from: accounts[0], gas: 4700000}).then(instance => {contractInstance = instance}
> contractInstance
- Interact with the deployed contract starting with adding candidates (Tristan and Rave) and sending the transactions from a different account (Account 1).
Remember, we have all accounts in our accounts[]
variable declared previously.
> contractInstance.methods.addCandidate('Tristan').send({from: accounts[1]}).then(result => console.log(result))
> contractInstance.methods.addCandidate('Rave').send({from: accounts[1]}).then(result => console.log(result))
We can see the information about the transactions that happened during the method executions and we can see that as well in our ganache terminal.
- Vote for Candidates:
> contractInstance.methods.voteForCandidate('Tristan').send({from: accounts[1]}).then(result => console.log(result))
> contractInstance.methods.voteForCandidate('Rave').send({from: accounts[2]}).then(result => console.log(result))
> contractInstance.methods.voteForCandidate('Tristan').send({from: accounts[3]}).then(result => console.log(result))
Let’s check Ganache again and see what happened there It should have some new transactions as writing to the contract costs ether.
- Check the votes for the candidates. Notice that we don’t use the
.send()
function here as reading from contracts isfree
, so we use.call()
instead.
Before we proceed, check the balances first:
> balances = accounts.map(account => web3.eth.getBalance(account))
Check the total votes using Account at index 5
:
> contractInstance.methods.totalVotesFor('Rave').call({from: accounts[5]}).then(result => console.log(result.toString()))
Promise { <pending> }
> 1
> contractInstance.methods.totalVotesFor('Tristan').call({from: accounts[5]}).then(result => console.log(result.toString()))
Promise { <pending> }
> 2
Check our balances again to make sure that this reading didn’t take any ETH from us.
> balances = accounts.map(account => web3.eth.getBalance(account))
And, it seems that there have been no new transactions.
We have determined that reading from a contract is free
.
MI4: Module 5: E1