├── chapter2 ├── .gitignore ├── app │ ├── .gitignore │ ├── package.json │ ├── webpack.config.js │ └── src │ │ ├── index.html │ │ └── index.js ├── .gitattributes ├── migrations │ ├── .2_deploy_contracts.js.swp │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── contracts │ ├── Migrations.sol │ └── Voting.sol └── truffle-config.js ├── chapter3 ├── .babelrc ├── .gitignore ├── .eslintignore ├── migrations │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── .eslintrc ├── contracts │ ├── Migrations.sol │ └── Voting.sol ├── truffle.js ├── app │ ├── stylesheets │ │ └── app.css │ ├── index.html │ └── javascripts │ │ └── app.js ├── webpack.config.js └── package.json ├── chapter4 ├── .babelrc ├── box-img-lg.png ├── box-img-sm.png ├── .eslintignore ├── migrations │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── app │ ├── stylesheets │ │ └── app.css │ ├── index.html │ └── javascripts │ │ └── app.js ├── truffle.js ├── .eslintrc ├── contracts │ ├── Migrations.sol │ ├── Voting.sol │ └── ECRecovery.sol ├── test │ ├── TestMetacoin.sol │ └── metacoin.js ├── webpack.config.js ├── test.js └── package.json ├── chapter1 ├── package.json ├── index.html ├── .gitignore ├── Voting.sol └── index.js └── README.md /chapter2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /chapter2/app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /chapter3/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /chapter4/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /chapter2/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /chapter3/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | yarn-error.log 4 | *.swp 5 | -------------------------------------------------------------------------------- /chapter4/box-img-lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maheshmurthy/ethereum_voting_dapp/HEAD/chapter4/box-img-lg.png -------------------------------------------------------------------------------- /chapter4/box-img-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maheshmurthy/ethereum_voting_dapp/HEAD/chapter4/box-img-sm.png -------------------------------------------------------------------------------- /chapter2/migrations/.2_deploy_contracts.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maheshmurthy/ethereum_voting_dapp/HEAD/chapter2/migrations/.2_deploy_contracts.js.swp -------------------------------------------------------------------------------- /chapter3/.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/** 2 | build/** 3 | node_modules/** 4 | contracts/** 5 | migrations/1_initial_migration.js 6 | migrations/2_deploy_contracts.js 7 | test/metacoin.js 8 | -------------------------------------------------------------------------------- /chapter4/.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/** 2 | build/** 3 | node_modules/** 4 | contracts/** 5 | migrations/1_initial_migration.js 6 | migrations/2_deploy_contracts.js 7 | test/metacoin.js 8 | -------------------------------------------------------------------------------- /chapter2/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /chapter3/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /chapter4/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /chapter4/app/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | background: #01bf86; 3 | padding-top: 3em; 4 | padding-bottom: 3em; 5 | color: white; 6 | } 7 | 8 | .col-margin-top-2 { 9 | margin-top: 2em; 10 | } 11 | -------------------------------------------------------------------------------- /chapter3/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var Voting = artifacts.require("./Voting.sol"); 2 | module.exports = function(deployer) { 3 | deployer.deploy(Voting, 1000, web3.toWei('0.1', 'ether'), ['Rama', 'Nick', 'Jose']); 4 | }; 5 | -------------------------------------------------------------------------------- /chapter2/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const Voting = artifacts.require("Voting"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'].map(name => web3.utils.asciiToHex(name))); 5 | }; 6 | -------------------------------------------------------------------------------- /chapter4/truffle.js: -------------------------------------------------------------------------------- 1 | // Allows us to use ES6 in our migrations and tests. 2 | require('babel-register') 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: '127.0.0.1', 8 | port: 8545, 9 | network_id: '*' // Match any network id 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapter3/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard" 5 | ], 6 | "plugins": [ 7 | "babel" 8 | ], 9 | "rules": { 10 | "key-spacing" : 0, 11 | "jsx-quotes" : [2, "prefer-single"], 12 | "max-len" : [2, 120, 2], 13 | "object-curly-spacing" : [2, "always"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter4/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard" 5 | ], 6 | "plugins": [ 7 | "babel" 8 | ], 9 | "rules": { 10 | "key-spacing" : 0, 11 | "jsx-quotes" : [2, "prefer-single"], 12 | "max-len" : [2, 120, 2], 13 | "object-curly-spacing" : [2, "always"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethereum_voting_dapp", 3 | "version": "1.0.0", 4 | "description": "Tutorial series by Mahesh Murthy: https://github.com/maheshmurthy/ethereum_voting_dapp", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ganache-cli": "^6.9.1", 8 | "solc": "^0.6.4", 9 | "web3": "^1.2.6" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "start": "node_modules/.bin/testrpc" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter2/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack", 8 | "dev": "webpack-dev-server" 9 | }, 10 | "devDependencies": { 11 | "copy-webpack-plugin": "^5.0.5", 12 | "webpack": "^4.41.2", 13 | "webpack-cli": "^3.3.10", 14 | "webpack-dev-server": "^3.9.0" 15 | }, 16 | "dependencies": { 17 | "web3": "^1.2.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /chapter2/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.7.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter2/app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: "./src/index.js", 7 | output: { 8 | filename: "index.js", 9 | path: path.resolve(__dirname, "dist"), 10 | }, 11 | plugins: [ 12 | new CopyWebpackPlugin([{ from: "./src/index.html", to: "index.html" }]), 13 | ], 14 | devServer: { contentBase: path.join(__dirname, "dist"), compress: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /chapter3/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chapter3/truffle.js: -------------------------------------------------------------------------------- 1 | // Allows us to use ES6 in our migrations and tests. 2 | require('babel-register') 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: 'localhost', 8 | port: 8545, 9 | gas: 6700000, 10 | network_id: '*' // Match any network id 11 | }, 12 | ropsten: { 13 | host: 'localhost', 14 | port: 8545, 15 | gas: 4700000, 16 | network_id: '3' // Match any network id 17 | }, 18 | kovan: { 19 | host: 'localhost', 20 | port: 8545, 21 | gas: 4700000, 22 | network_id: '5' // Match any network id 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter4/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum Voting Dapp 2 | Simple Ethereum Voting dapp using Truffle framework 3 | 4 | ## Usage 5 | The below blog posts explain each chapter in detail: 6 | 7 | Chapter 1: https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2#.yqxqj0hff 8 | 9 | Chapter 2: https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-2-30b3d335aa1f 10 | 11 | Chapter 3: https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-3-331c2712c9df#.ldky416p7 12 | 13 | Chapter 4: https://medium.com/zastrin/how-to-save-your-ethereum-dapp-users-from-paying-gas-for-transactions-abd72f15e14d 14 | -------------------------------------------------------------------------------- /chapter4/test/TestMetacoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/MetaCoin.sol"; 6 | 7 | contract TestMetacoin { 8 | 9 | function testInitialBalanceUsingDeployedContract() { 10 | MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin()); 11 | 12 | uint expected = 10000; 13 | 14 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 15 | } 16 | 17 | function testInitialBalanceWithNewMetaCoin() { 18 | MetaCoin meta = new MetaCoin(); 19 | 20 | uint expected = 10000; 21 | 22 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /chapter4/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var Voting = artifacts.require("./Voting.sol"); 2 | var ECRecovery = artifacts.require("./ECRecovery.sol"); 3 | 4 | const sigUtil = require("eth-sig-util") 5 | 6 | var alice_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Alice"}]) 7 | var bob_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Bob"}]) 8 | var carol_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Carol"}]) 9 | 10 | module.exports = function(deployer) { 11 | deployer.deploy(ECRecovery); 12 | deployer.link(ECRecovery, Voting); 13 | deployer.deploy(Voting, ['Alice', 'Bob', 'Carol'], [alice_vote_hash, bob_vote_hash, carol_vote_hash]); 14 | }; 15 | -------------------------------------------------------------------------------- /chapter3/app/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-left: 25%; 3 | margin-right: 25%; 4 | margin-top: 2%; 5 | font-family: "Open Sans", sans-serif; 6 | } 7 | 8 | label { 9 | display: inline-block; 10 | width: 100px; 11 | } 12 | 13 | input { 14 | width: 500px; 15 | padding: 5px; 16 | font-size: 16px; 17 | } 18 | 19 | button { 20 | font-size: 16px; 21 | padding: 5px; 22 | } 23 | 24 | h1, h2 { 25 | display: inline-block; 26 | vertical-align: middle; 27 | margin-top: 0px; 28 | margin-bottom: 10px; 29 | } 30 | 31 | h2 { 32 | color: #AAA; 33 | font-size: 32px; 34 | } 35 | 36 | h3 { 37 | font-weight: normal; 38 | color: #AAA; 39 | font-size: 24px; 40 | } 41 | 42 | .black { 43 | color: black; 44 | } 45 | 46 | #balance { 47 | color: black; 48 | } 49 | 50 | .hint { 51 | color: #666; 52 | } 53 | -------------------------------------------------------------------------------- /chapter3/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './app/javascripts/app.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'app.js' 9 | }, 10 | plugins: [ 11 | // Copy our app's index.html to the build folder. 12 | new CopyWebpackPlugin([ 13 | { from: './app/index.html', to: "index.html" } 14 | ]) 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: [ 'style-loader', 'css-loader' ] 21 | } 22 | ], 23 | loaders: [ 24 | { test: /\.json$/, use: 'json-loader' }, 25 | { 26 | test: /\.js$/, 27 | exclude: /(node_modules|bower_components)/, 28 | loader: 'babel-loader', 29 | query: { 30 | presets: ['es2015'], 31 | plugins: ['transform-runtime'] 32 | } 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /chapter4/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './app/javascripts/app.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'app.js' 9 | }, 10 | plugins: [ 11 | // Copy our app's index.html to the build folder. 12 | new CopyWebpackPlugin([ 13 | { from: './app/index.html', to: "index.html" } 14 | ]) 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: [ 'style-loader', 'css-loader' ] 21 | } 22 | ], 23 | loaders: [ 24 | { test: /\.json$/, use: 'json-loader' }, 25 | { 26 | test: /\.js$/, 27 | exclude: /(node_modules|bower_components)/, 28 | loader: 'babel-loader', 29 | query: { 30 | presets: ['es2015'], 31 | plugins: ['transform-runtime'] 32 | } 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /chapter3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truffle-init-webpack", 3 | "version": "0.0.1", 4 | "description": "Frontend example using truffle v3", 5 | "scripts": { 6 | "lint": "eslint ./", 7 | "build": "webpack", 8 | "dev": "webpack-dev-server" 9 | }, 10 | "author": "Douglas von Kohorn", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-cli": "^6.22.2", 14 | "babel-core": "^6.22.1", 15 | "babel-eslint": "^6.1.2", 16 | "babel-loader": "^6.2.10", 17 | "babel-plugin-transform-runtime": "^6.22.0", 18 | "babel-preset-env": "^1.1.8", 19 | "babel-preset-es2015": "^6.22.0", 20 | "babel-register": "^6.22.0", 21 | "copy-webpack-plugin": "^4.0.1", 22 | "css-loader": "^0.26.1", 23 | "eslint": "^3.14.0", 24 | "eslint-config-standard": "^6.0.0", 25 | "eslint-plugin-babel": "^4.0.0", 26 | "eslint-plugin-mocha": "^4.8.0", 27 | "eslint-plugin-promise": "^3.0.0", 28 | "eslint-plugin-standard": "^2.0.0", 29 | "html-webpack-plugin": "^2.28.0", 30 | "json-loader": "^0.5.4", 31 | "style-loader": "^0.13.1", 32 | "truffle-contract": "^1.1.6", 33 | "web3": "^0.18.2", 34 | "webpack": "^2.2.1", 35 | "webpack-dev-server": "^2.3.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chapter1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World DApp 5 | 6 | 7 | 8 | 9 |

A Simple Hello World Voting Application

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
CandidateVotes
Rama
Nick
Jose
33 |
34 | 35 | Vote 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /chapter1/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | 65 | # End of https://www.gitignore.io/api/node -------------------------------------------------------------------------------- /chapter2/app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World DApp 5 | 6 | 7 | 8 | 9 |

A Simple Hello World Voting Application

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
CandidateVotes
Rama
Nick
Jose
33 |
34 | 35 | Vote 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /chapter4/contracts/Voting.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | import "./ECRecovery.sol"; 3 | 4 | contract Voting { 5 | using ECRecovery for bytes32; 6 | 7 | mapping (bytes32 => uint8) public votesReceived; 8 | 9 | mapping(bytes32 => bytes32) public candidateHash; 10 | 11 | mapping(address => bool) public voterStatus; 12 | 13 | mapping(bytes32 => bool) public validCandidates; 14 | 15 | function Voting(bytes32[] _candidateNames, bytes32[] _candidateHashes) public { 16 | for(uint i = 0; i < _candidateNames.length; i++) { 17 | validCandidates[_candidateNames[i]] = true; 18 | candidateHash[_candidateNames[i]] = _candidateHashes[i]; 19 | } 20 | } 21 | 22 | function totalVotesFor(bytes32 _candidate) view public returns (uint8) { 23 | require(validCandidates[_candidate]); 24 | return votesReceived[_candidate]; 25 | } 26 | 27 | function voteForCandidate(bytes32 _candidate, address _voter, bytes _signedMessage) public { 28 | require(!voterStatus[_voter]); 29 | 30 | bytes32 voteHash = candidateHash[_candidate]; 31 | address recoveredAddress = voteHash.recover(_signedMessage); 32 | 33 | require(recoveredAddress == _voter); 34 | require(validCandidates[_candidate]); 35 | 36 | votesReceived[_candidate] += 1; 37 | voterStatus[_voter] = true; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /chapter4/test.js: -------------------------------------------------------------------------------- 1 | // MyContract 2 | import './ECRecovery.sol'; // copy this from zeppelin-solidity/contracts 3 | pragma solidity ^0.4.18; 4 | contract MyContract { 5 | using ECRecovery for bytes32; 6 | function recover(bytes32 hash, bytes sig) public pure returns (address) { 7 | return hash.recover(sig); 8 | } 9 | } 10 | 11 | // migrations/2_MyContract.js 12 | const ECRecovery = artifacts.require('./ECRecovery.sol') 13 | const MyContract = artifacts.require('./MyContract.sol') 14 | 15 | module.exports = function (deployer) { 16 | deployer.deploy(ECRecovery) 17 | deployer.link(ECRecovery, MyContract) 18 | } 19 | 20 | // test code 21 | import hashMessage from './helpers/hashMessage.js' // copy this from zeppelin-solidity/test/helpers 22 | const MyContract = artifacts.require('MyContract') 23 | 24 | contract('MyContract', accounts => { 25 | 26 | it('Test recover signer', async() => { 27 | contractInstance = await MyContract.deployed() 28 | const message = 'Hello Alice' 29 | const signAddress = web3.eth.accounts[0] 30 | const sig = web3.eth.sign(signAddress, web3.sha3(message)) 31 | const hash = hashMessage(message) 32 | const result = await contractInstance.recover(hash, sig) 33 | assert.equal(signAddress, result, 'Recovered address should be same as signAddress') 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /chapter4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truffle-init-webpack", 3 | "version": "0.0.2", 4 | "description": "Frontend example using truffle v3", 5 | "scripts": { 6 | "lint": "eslint ./", 7 | "build": "webpack", 8 | "dev": "webpack-dev-server" 9 | }, 10 | "author": "Douglas von Kohorn", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-cli": "^6.22.2", 14 | "babel-core": "^6.22.1", 15 | "babel-eslint": "^6.1.2", 16 | "babel-loader": "^6.2.10", 17 | "babel-plugin-transform-runtime": "^6.22.0", 18 | "babel-preset-env": "^1.1.8", 19 | "babel-preset-es2015": "^6.22.0", 20 | "babel-register": "^6.22.0", 21 | "copy-webpack-plugin": "^4.0.1", 22 | "css-loader": "^0.26.1", 23 | "eslint": "^3.14.0", 24 | "eslint-config-standard": "^6.0.0", 25 | "eslint-plugin-babel": "^4.0.0", 26 | "eslint-plugin-mocha": "^4.8.0", 27 | "eslint-plugin-promise": "^3.0.0", 28 | "eslint-plugin-standard": "^2.0.0", 29 | "eth-sig-util": "1.4.2", 30 | "ethereumjs-util": "5.1.2", 31 | "html-webpack-plugin": "^2.28.0", 32 | "json-loader": "^0.5.4", 33 | "style-loader": "^0.13.1", 34 | "truffle-contract": "^1.1.11", 35 | "web3": "^0.20.0", 36 | "webpack": "^2.2.1", 37 | "webpack-cli": "^2.0.14", 38 | "webpack-dev-server": "^2.3.0", 39 | "bip39": "^2.4.0", 40 | "ethereumjs-wallet": "^0.6.0", 41 | "truffle": "^4.0.1", 42 | "web3-provider-engine": "^13.3.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapter4/contracts/ECRecovery.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title Eliptic curve signature operations 6 | * 7 | * @dev Based on https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d 8 | * 9 | * TODO Remove this library once solidity supports passing a signature to ecrecover. 10 | * See https://github.com/ethereum/solidity/issues/864 11 | * 12 | */ 13 | 14 | library ECRecovery { 15 | 16 | /** 17 | * @dev Recover signer address from a message by using their signature 18 | * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. 19 | * @param sig bytes signature, the signature is generated using web3.eth.sign() 20 | */ 21 | function recover(bytes32 hash, bytes sig) internal pure returns (address) { 22 | bytes32 r; 23 | bytes32 s; 24 | uint8 v; 25 | 26 | //Check the signature length 27 | if (sig.length != 65) { 28 | return (address(0)); 29 | } 30 | 31 | // Divide the signature in r, s and v variables 32 | // ecrecover takes the signature parameters, and the only way to get them 33 | // currently is to use assembly. 34 | // solium-disable-next-line security/no-inline-assembly 35 | assembly { 36 | r := mload(add(sig, 32)) 37 | s := mload(add(sig, 64)) 38 | v := byte(0, mload(add(sig, 96))) 39 | } 40 | 41 | // Version of signature should be 27 or 28, but 0 and 1 are also possible versions 42 | if (v < 27) { 43 | v += 27; 44 | } 45 | 46 | // If the version is correct return the signer address 47 | if (v != 27 && v != 28) { 48 | return (address(0)); 49 | } else { 50 | return ecrecover(hash, v, r, s); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /chapter1/Voting.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.4; 2 | // We have to specify what version of compiler this code will compile with 3 | 4 | contract Voting { 5 | /* mapping field below is equivalent to an associative array or hash. 6 | The key of the mapping is candidate name stored as type bytes32 and value is 7 | an unsigned integer to store the vote count 8 | */ 9 | 10 | mapping (bytes32 => uint256) public votesReceived; 11 | 12 | /* Solidity doesn't let you pass in an array of strings in the constructor (yet). 13 | We will use an array of bytes32 instead to store the list of candidates 14 | */ 15 | 16 | bytes32[] public candidateList; 17 | 18 | /* This is the constructor which will be called once when you 19 | deploy the contract to the blockchain. When we deploy the contract, 20 | we will pass an array of candidates who will be contesting in the election 21 | */ 22 | constructor(bytes32[] memory candidateNames) public { 23 | candidateList = candidateNames; 24 | } 25 | 26 | // This function returns the total votes a candidate has received so far 27 | function totalVotesFor(bytes32 candidate) view public returns (uint256) { 28 | require(validCandidate(candidate)); 29 | return votesReceived[candidate]; 30 | } 31 | 32 | // This function increments the vote count for the specified candidate. This 33 | // is equivalent to casting a vote 34 | function voteForCandidate(bytes32 candidate) public { 35 | require(validCandidate(candidate)); 36 | votesReceived[candidate] += 1; 37 | } 38 | 39 | function validCandidate(bytes32 candidate) view public returns (bool) { 40 | for(uint i = 0; i < candidateList.length; i++) { 41 | if (candidateList[i] == candidate) { 42 | return true; 43 | } 44 | } 45 | return false; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /chapter2/contracts/Voting.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.4; 2 | // We have to specify what version of compiler this code will compile with 3 | 4 | contract Voting { 5 | /* mapping field below is equivalent to an associative array or hash. 6 | The key of the mapping is candidate name stored as type bytes32 and value is 7 | an unsigned integer to store the vote count 8 | */ 9 | 10 | mapping (bytes32 => uint256) public votesReceived; 11 | 12 | /* Solidity doesn't let you pass in an array of strings in the constructor (yet). 13 | We will use an array of bytes32 instead to store the list of candidates 14 | */ 15 | 16 | bytes32[] public candidateList; 17 | 18 | /* This is the constructor which will be called once when you 19 | deploy the contract to the blockchain. When we deploy the contract, 20 | we will pass an array of candidates who will be contesting in the election 21 | */ 22 | constructor(bytes32[] memory candidateNames) public { 23 | candidateList = candidateNames; 24 | } 25 | 26 | // This function returns the total votes a candidate has received so far 27 | function totalVotesFor(bytes32 candidate) view public returns (uint256) { 28 | require(validCandidate(candidate)); 29 | return votesReceived[candidate]; 30 | } 31 | 32 | // This function increments the vote count for the specified candidate. This 33 | // is equivalent to casting a vote 34 | function voteForCandidate(bytes32 candidate) public { 35 | require(validCandidate(candidate)); 36 | votesReceived[candidate] += 1; 37 | } 38 | 39 | function validCandidate(bytes32 candidate) view public returns (bool) { 40 | for(uint i = 0; i < candidateList.length; i++) { 41 | if (candidateList[i] == candidate) { 42 | return true; 43 | } 44 | } 45 | return false; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /chapter1/index.js: -------------------------------------------------------------------------------- 1 | web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) 2 | var account; 3 | web3.eth.getAccounts().then((f) => { 4 | account = f[0]; 5 | }) 6 | 7 | abi = JSON.parse('[{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]') 8 | 9 | contract = new web3.eth.Contract(abi); 10 | contract.options.address = "0x0734ea3C54dE23448fD3dfbE20b388988506BfAC"; 11 | // update this contract address with your contract address 12 | 13 | candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"} 14 | 15 | function voteForCandidate(candidate) { 16 | candidateName = $("#candidate").val(); 17 | console.log(candidateName); 18 | 19 | contract.methods.voteForCandidate(web3.utils.asciiToHex(candidateName)).send({from: account}).then((f) => { 20 | let div_id = candidates[candidateName]; 21 | contract.methods.totalVotesFor(web3.utils.asciiToHex(candidateName)).call().then((f) => { 22 | $("#" + div_id).html(f); 23 | }) 24 | }) 25 | } 26 | 27 | $(document).ready(function() { 28 | candidateNames = Object.keys(candidates); 29 | 30 | for(var i=0; i { 34 | $("#" + candidates[name]).html(f); 35 | }) 36 | } 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /chapter4/test/metacoin.js: -------------------------------------------------------------------------------- 1 | var MetaCoin = artifacts.require("./MetaCoin.sol"); 2 | 3 | contract('MetaCoin', function(accounts) { 4 | it("should put 10000 MetaCoin in the first account", function() { 5 | return MetaCoin.deployed().then(function(instance) { 6 | return instance.getBalance.call(accounts[0]); 7 | }).then(function(balance) { 8 | assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); 9 | }); 10 | }); 11 | it("should call a function that depends on a linked library", function() { 12 | var meta; 13 | var metaCoinBalance; 14 | var metaCoinEthBalance; 15 | 16 | return MetaCoin.deployed().then(function(instance) { 17 | meta = instance; 18 | return meta.getBalance.call(accounts[0]); 19 | }).then(function(outCoinBalance) { 20 | metaCoinBalance = outCoinBalance.toNumber(); 21 | return meta.getBalanceInEth.call(accounts[0]); 22 | }).then(function(outCoinBalanceEth) { 23 | metaCoinEthBalance = outCoinBalanceEth.toNumber(); 24 | }).then(function() { 25 | assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpeced function, linkage may be broken"); 26 | }); 27 | }); 28 | 29 | it("should send coin correctly", function() { 30 | var meta; 31 | 32 | // Get initial balances of first and second account. 33 | var account_one = accounts[0]; 34 | var account_two = accounts[1]; 35 | 36 | var account_one_starting_balance; 37 | var account_two_starting_balance; 38 | var account_one_ending_balance; 39 | var account_two_ending_balance; 40 | 41 | var amount = 10; 42 | 43 | return MetaCoin.deployed().then(function(instance) { 44 | meta = instance; 45 | return meta.getBalance.call(account_one); 46 | }).then(function(balance) { 47 | account_one_starting_balance = balance.toNumber(); 48 | return meta.getBalance.call(account_two); 49 | }).then(function(balance) { 50 | account_two_starting_balance = balance.toNumber(); 51 | return meta.sendCoin(account_two, amount, {from: account_one}); 52 | }).then(function() { 53 | return meta.getBalance.call(account_one); 54 | }).then(function(balance) { 55 | account_one_ending_balance = balance.toNumber(); 56 | return meta.getBalance.call(account_two); 57 | }).then(function(balance) { 58 | account_two_ending_balance = balance.toNumber(); 59 | 60 | assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender"); 61 | assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver"); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /chapter3/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World DApp 5 | 6 | 7 | 12 | 13 | 14 |

A Simple Hello World Voting Application

15 |
16 |

Candidates

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
CandidateVotes
28 |
29 |
30 |

Vote for Candidate

31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | Vote 39 |
40 |
41 |
42 |
43 |

Token Stats

44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
Tokens For Sale
Tokens Sold
Price Per Token
Balance in the contract
63 |
64 |
65 |
66 |

Purchase Tokens

67 |
68 |
69 |   70 | Buy 71 |
72 |
73 |
74 |

Lookup Voter Info

75 |
76 |   77 | Lookup 78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /chapter2/app/src/index.js: -------------------------------------------------------------------------------- 1 | import Web3 from "web3"; 2 | import votingArtifact from "../../build/contracts/Voting.json"; 3 | 4 | let candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"} 5 | 6 | const App = { 7 | web3: null, 8 | account: null, 9 | voting: null, 10 | 11 | start: async function() { 12 | const { web3 } = this; 13 | 14 | try { 15 | /* Get the network we are connected to and then read the build/contracts/Voting.json and instantiate a contract object to use 16 | */ 17 | 18 | // 19 | const networkId = await web3.eth.net.getId(); 20 | const deployedNetwork = votingArtifact.networks[networkId]; 21 | this.voting = new web3.eth.Contract( 22 | votingArtifact.abi, 23 | deployedNetwork.address, 24 | ); 25 | 26 | // get accounts 27 | const accounts = await web3.eth.getAccounts(); 28 | this.account = accounts[0]; 29 | 30 | this.loadCandidatesAndVotes(); 31 | } catch (error) { 32 | console.error("Could not connect to contract or chain."); 33 | } 34 | }, 35 | 36 | loadCandidatesAndVotes: async function() { 37 | const { totalVotesFor } = this.voting.methods; 38 | //const { sendCoin } = this.meta.methods; 39 | //await sendCoin(receiver, amount).send({ from: this.account }); 40 | let candidateNames = Object.keys(candidates); 41 | for (var i = 0; i < candidateNames.length; i++) { 42 | let name = candidateNames[i]; 43 | var count = await totalVotesFor(this.web3.utils.asciiToHex(name)).call(); 44 | $("#" + candidates[name]).html(count); 45 | } 46 | }, 47 | 48 | voteForCandidate: async function() { 49 | let candidateName = $("#candidate").val(); 50 | $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.") 51 | $("#candidate").val(""); 52 | 53 | const { totalVotesFor, voteForCandidate } = this.voting.methods; 54 | 55 | 56 | /* Voting.deployed() returns an instance of the contract. Every call 57 | * in Truffle returns a promise which is why we have used then() 58 | * everywhere we have a transaction call 59 | */ 60 | await voteForCandidate(this.web3.utils.asciiToHex(candidateName)).send({gas: 140000, from: this.account}); 61 | let div_id = candidates[candidateName]; 62 | var count = await totalVotesFor(this.web3.utils.asciiToHex(candidateName)).call(); 63 | $("#" + div_id).html(count); 64 | $("#msg").html(""); 65 | } 66 | 67 | }; 68 | 69 | window.App = App; 70 | 71 | window.addEventListener("load", function() { 72 | if (window.ethereum) { 73 | // use MetaMask's provider 74 | App.web3 = new Web3(window.ethereum); 75 | window.ethereum.enable(); // get permission to access accounts 76 | } else { 77 | console.warn( 78 | "No web3 detected. Falling back to http://127.0.0.1:8545. You should remove this fallback when you deploy live", 79 | ); 80 | // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) 81 | App.web3 = new Web3( 82 | new Web3.providers.HttpProvider("http://127.0.0.1:8545"), 83 | ); 84 | } 85 | 86 | App.start(); 87 | }); 88 | 89 | 90 | -------------------------------------------------------------------------------- /chapter4/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World DApp 5 | 6 | 7 | 8 | 9 | 10 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
CandidateVotes
Alice
Bob
Carol
40 |
41 |
42 |
43 |
44 |

Vote for your favorite candidate

45 |
46 |
47 | 48 | 49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |

Submit vote to blockchain

62 |
63 |
64 | 65 |
66 | 67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 | 77 |
78 | 79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /chapter4/app/javascripts/app.js: -------------------------------------------------------------------------------- 1 | // Import the page's CSS. Webpack will know what to do with it. 2 | import "../stylesheets/app.css"; 3 | 4 | // Import libraries we need. 5 | import { default as Web3} from 'web3'; 6 | import { default as contract } from 'truffle-contract'; 7 | import { default as ethUtil} from 'ethereumjs-util'; 8 | import { default as sigUtil} from 'eth-sig-util'; 9 | 10 | 11 | /* 12 | * When you compile and deploy your Voting contract, 13 | * truffle stores the abi and deployed address in a json 14 | * file in the build directory. We will use this information 15 | * to setup a Voting abstraction. We will use this abstraction 16 | * later to create an instance of the Voting contract. 17 | * Compare this against the index.js from our previous tutorial to see the difference 18 | * https://gist.github.com/maheshmurthy/f6e96d6b3fff4cd4fa7f892de8a1a1b4#file-index-js 19 | */ 20 | 21 | import voting_artifacts from '../../build/contracts/Voting.json' 22 | 23 | var Voting = contract(voting_artifacts); 24 | 25 | let candidates = {"Alice": "candidate-1", "Bob": "candidate-2", "Carol": "candidate-3"} 26 | 27 | window.submitVote = function(candidate) { 28 | let candidateName = $("#candidate-name").val(); 29 | let signature = $("#voter-signature").val(); 30 | let voterAddress = $("#voter-address").val(); 31 | 32 | console.log(candidateName); 33 | console.log(signature); 34 | console.log(voterAddress); 35 | 36 | $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.") 37 | 38 | Voting.deployed().then(function(contractInstance) { 39 | contractInstance.voteForCandidate(candidateName, voterAddress, signature, {gas: 140000, from: web3.eth.accounts[0]}).then(function() { 40 | let div_id = candidates[candidateName]; 41 | console.log(div_id); 42 | return contractInstance.totalVotesFor.call(candidateName).then(function(v) { 43 | console.log(v.toString()); 44 | $("#" + div_id).html(v.toString()); 45 | $("#msg").html(""); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | window.voteForCandidate = function(candidate) { 52 | let candidateName = $("#candidate").val(); 53 | 54 | let msgParams = [ 55 | { 56 | type: 'string', // Any valid solidity type 57 | name: 'Message', // Any string label you want 58 | value: 'Vote for ' + candidateName // The value to sign 59 | } 60 | ] 61 | 62 | var from = web3.eth.accounts[0] 63 | 64 | var params = [msgParams, from] 65 | var method = 'eth_signTypedData' 66 | 67 | console.log("Hash is "); 68 | console.log(sigUtil.typedSignatureHash(msgParams)); 69 | 70 | web3.currentProvider.sendAsync({ 71 | method, 72 | params, 73 | from, 74 | }, function (err, result) { 75 | if (err) return console.dir(err) 76 | if (result.error) { 77 | alert(result.error.message) 78 | } 79 | if (result.error) return console.error(result) 80 | $("#msg").html("User wants to vote for " + candidateName + ". Any one can now submit the vote to the blockchain on behalf of this user. Use the below values to submit the vote to the blockchain"); 81 | $("#vote-for").html("Candidate: " + candidateName); 82 | $("#addr").html("Address: " + from); 83 | $("#signature").html("Signature: " + result.result); 84 | console.log('PERSONAL SIGNED:' + JSON.stringify(result.result)) 85 | }) 86 | } 87 | 88 | $( document ).ready(function() { 89 | if (typeof web3 !== 'undefined') { 90 | console.warn("Using web3 detected from external source like Metamask") 91 | // Use Mist/MetaMask's provider 92 | window.web3 = new Web3(web3.currentProvider); 93 | } else { 94 | console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask"); 95 | // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) 96 | window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 97 | } 98 | 99 | Voting.setProvider(web3.currentProvider); 100 | let candidateNames = Object.keys(candidates); 101 | for (var i = 0; i < candidateNames.length; i++) { 102 | let name = candidateNames[i]; 103 | Voting.deployed().then(function(contractInstance) { 104 | contractInstance.totalVotesFor.call(name).then(function(v) { 105 | $("#" + candidates[name]).html(v.toString()); 106 | }); 107 | }) 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /chapter2/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWallet = require('truffle-hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | // development: { 46 | // host: "127.0.0.1", // Localhost (default: none) 47 | // port: 8545, // Standard Ethereum port (default: none) 48 | // network_id: "*", // Any network (default: none) 49 | // }, 50 | 51 | develop: { 52 | host: 'localhost', 53 | port: 8545, 54 | gas: 6700000, 55 | network_id: '*' 56 | }, 57 | 58 | // Another network with more advanced options... 59 | // advanced: { 60 | // port: 8777, // Custom port 61 | // network_id: 1342, // Custom network 62 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 63 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 64 | // from:
, // Account to send txs from (default: accounts[0]) 65 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 66 | // }, 67 | 68 | // Useful for deploying to a public network. 69 | // NB: It's important to wrap the provider as a function. 70 | // ropsten: { 71 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 72 | // network_id: 3, // Ropsten's id 73 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 74 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 75 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 76 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 77 | // }, 78 | 79 | // Useful for private networks 80 | // private: { 81 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 82 | // network_id: 2111, // This network is yours, in the cloud. 83 | // production: true // Treats this network as if it was a public net. (default: false) 84 | // } 85 | }, 86 | 87 | // Set default mocha options here, use special reporters etc. 88 | mocha: { 89 | // timeout: 100000 90 | }, 91 | 92 | // Configure your compilers 93 | compilers: { 94 | solc: { 95 | version: "0.6.4", // Fetch exact version from solc-bin (default: truffle's version) 96 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 97 | // settings: { // See the solidity docs for advice about optimization and evmVersion 98 | // optimizer: { 99 | // enabled: false, 100 | // runs: 200 101 | // }, 102 | // evmVersion: "byzantium" 103 | // } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /chapter3/contracts/Voting.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; //We have to specify what version of the compiler this code will use 2 | 3 | contract Voting { 4 | 5 | // We use the struct datatype to store the voter information. 6 | struct voter { 7 | address voterAddress; // The address of the voter 8 | uint tokensBought; // The total no. of tokens this voter owns 9 | uint[] tokensUsedPerCandidate; // Array to keep track of votes per candidate. 10 | /* We have an array of candidates initialized below. 11 | Every time this voter votes with her tokens, the value at that 12 | index is incremented. Example, if candidateList array declared 13 | below has ["Rama", "Nick", "Jose"] and this 14 | voter votes 10 tokens to Nick, the tokensUsedPerCandidate[1] 15 | will be incremented by 10. 16 | */ 17 | } 18 | 19 | /* mapping is equivalent to an associate array or hash 20 | The key of the mapping is candidate name stored as type bytes32 and value is 21 | an unsigned integer which used to store the vote count 22 | */ 23 | 24 | mapping (address => voter) public voterInfo; 25 | 26 | /* Solidity doesn't let you return an array of strings yet. We will use an array of bytes32 27 | instead to store the list of candidates 28 | */ 29 | 30 | mapping (bytes32 => uint) public votesReceived; 31 | 32 | bytes32[] public candidateList; 33 | 34 | uint public totalTokens; // Total no. of tokens available for this election 35 | uint public balanceTokens; // Total no. of tokens still available for purchase 36 | uint public tokenPrice; // Price per token 37 | 38 | /* When the contract is deployed on the blockchain, we will initialize 39 | the total number of tokens for sale, cost per token and all the candidates 40 | */ 41 | function Voting(uint tokens, uint pricePerToken, bytes32[] candidateNames) public { 42 | candidateList = candidateNames; 43 | totalTokens = tokens; 44 | balanceTokens = tokens; 45 | tokenPrice = pricePerToken; 46 | } 47 | 48 | function totalVotesFor(bytes32 candidate) view public returns (uint) { 49 | return votesReceived[candidate]; 50 | } 51 | 52 | /* Instead of just taking the candidate name as an argument, we now also 53 | require the no. of tokens this voter wants to vote for the candidate 54 | */ 55 | function voteForCandidate(bytes32 candidate, uint votesInTokens) public { 56 | uint index = indexOfCandidate(candidate); 57 | require(index != uint(-1)); 58 | 59 | // msg.sender gives us the address of the account/voter who is trying 60 | // to call this function 61 | if (voterInfo[msg.sender].tokensUsedPerCandidate.length == 0) { 62 | for(uint i = 0; i < candidateList.length; i++) { 63 | voterInfo[msg.sender].tokensUsedPerCandidate.push(0); 64 | } 65 | } 66 | 67 | // Make sure this voter has enough tokens to cast the vote 68 | uint availableTokens = voterInfo[msg.sender].tokensBought - totalTokensUsed(voterInfo[msg.sender].tokensUsedPerCandidate); 69 | require(availableTokens >= votesInTokens); 70 | 71 | votesReceived[candidate] += votesInTokens; 72 | 73 | // Store how many tokens were used for this candidate 74 | voterInfo[msg.sender].tokensUsedPerCandidate[index] += votesInTokens; 75 | } 76 | 77 | // Return the sum of all the tokens used by this voter. 78 | function totalTokensUsed(uint[] _tokensUsedPerCandidate) private pure returns (uint) { 79 | uint totalUsedTokens = 0; 80 | for(uint i = 0; i < _tokensUsedPerCandidate.length; i++) { 81 | totalUsedTokens += _tokensUsedPerCandidate[i]; 82 | } 83 | return totalUsedTokens; 84 | } 85 | 86 | function indexOfCandidate(bytes32 candidate) view public returns (uint) { 87 | for(uint i = 0; i < candidateList.length; i++) { 88 | if (candidateList[i] == candidate) { 89 | return i; 90 | } 91 | } 92 | return uint(-1); 93 | } 94 | 95 | /* This function is used to purchase the tokens. Note the keyword 'payable' 96 | below. By just adding that one keyword to a function, your contract can 97 | now accept Ether from anyone who calls this function. Accepting money can 98 | not get any easier than this! 99 | */ 100 | 101 | function buy() payable public returns (uint) { 102 | uint tokensToBuy = msg.value / tokenPrice; 103 | require(tokensToBuy <= balanceTokens); 104 | voterInfo[msg.sender].voterAddress = msg.sender; 105 | voterInfo[msg.sender].tokensBought += tokensToBuy; 106 | balanceTokens -= tokensToBuy; 107 | return tokensToBuy; 108 | } 109 | 110 | function tokensSold() view public returns (uint) { 111 | return totalTokens - balanceTokens; 112 | } 113 | 114 | function voterDetails(address user) view public returns (uint, uint[]) { 115 | return (voterInfo[user].tokensBought, voterInfo[user].tokensUsedPerCandidate); 116 | } 117 | 118 | /* All the ether sent by voters who purchased the tokens is in this 119 | contract's account. This method will be used to transfer out all those ethers 120 | in to another account. *** The way this function is written currently, anyone can call 121 | this method and transfer the balance in to their account. In reality, you should add 122 | check to make sure only the owner of this contract can cash out. 123 | */ 124 | 125 | function transferTo(address account) public { 126 | account.transfer(this.balance); 127 | } 128 | 129 | function allCandidates() view public returns (bytes32[]) { 130 | return candidateList; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /chapter3/app/javascripts/app.js: -------------------------------------------------------------------------------- 1 | // Import the page's CSS. Webpack will know what to do with it. 2 | import "../stylesheets/app.css"; 3 | 4 | // Import libraries we need. 5 | import { default as Web3} from 'web3'; 6 | import { default as contract } from 'truffle-contract' 7 | 8 | /* 9 | * When you compile and deploy your Voting contract, 10 | * truffle stores the abi and deployed address in a json 11 | * file in the build directory. We will use this information 12 | * to setup a Voting abstraction. We will use this abstraction 13 | * later to create an instance of the Voting contract. 14 | * Compare this against the index.js from our previous tutorial to see the difference 15 | * https://gist.github.com/maheshmurthy/f6e96d6b3fff4cd4fa7f892de8a1a1b4#file-index-js 16 | */ 17 | 18 | import voting_artifacts from '../../build/contracts/Voting.json' 19 | 20 | var Voting = contract(voting_artifacts); 21 | 22 | let candidates = {} 23 | 24 | let tokenPrice = null; 25 | 26 | window.voteForCandidate = function(candidate) { 27 | let candidateName = $("#candidate").val(); 28 | let voteTokens = $("#vote-tokens").val(); 29 | $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.") 30 | $("#candidate").val(""); 31 | $("#vote-tokens").val(""); 32 | 33 | /* Voting.deployed() returns an instance of the contract. Every call 34 | * in Truffle returns a promise which is why we have used then() 35 | * everywhere we have a transaction call 36 | */ 37 | Voting.deployed().then(function(contractInstance) { 38 | contractInstance.voteForCandidate(candidateName, voteTokens, {gas: 140000, from: web3.eth.accounts[0]}).then(function() { 39 | let div_id = candidates[candidateName]; 40 | return contractInstance.totalVotesFor.call(candidateName).then(function(v) { 41 | $("#" + div_id).html(v.toString()); 42 | $("#msg").html(""); 43 | }); 44 | }); 45 | }); 46 | } 47 | 48 | /* The user enters the total no. of tokens to buy. We calculate the total cost and send it in 49 | * the request. We have to send the value in Wei. So, we use the toWei helper method to convert 50 | * from Ether to Wei. 51 | */ 52 | 53 | window.buyTokens = function() { 54 | let tokensToBuy = $("#buy").val(); 55 | let price = tokensToBuy * tokenPrice; 56 | $("#buy-msg").html("Purchase order has been submitted. Please wait."); 57 | Voting.deployed().then(function(contractInstance) { 58 | contractInstance.buy({value: web3.toWei(price, 'ether'), from: web3.eth.accounts[0]}).then(function(v) { 59 | $("#buy-msg").html(""); 60 | web3.eth.getBalance(contractInstance.address, function(error, result) { 61 | $("#contract-balance").html(web3.fromWei(result.toString()) + " Ether"); 62 | }); 63 | }) 64 | }); 65 | populateTokenData(); 66 | } 67 | 68 | window.lookupVoterInfo = function() { 69 | let address = $("#voter-info").val(); 70 | Voting.deployed().then(function(contractInstance) { 71 | contractInstance.voterDetails.call(address).then(function(v) { 72 | $("#tokens-bought").html("Total Tokens bought: " + v[0].toString()); 73 | let votesPerCandidate = v[1]; 74 | $("#votes-cast").empty(); 75 | $("#votes-cast").append("Votes cast per candidate:
"); 76 | let allCandidates = Object.keys(candidates); 77 | for(let i=0; i < allCandidates.length; i++) { 78 | $("#votes-cast").append(allCandidates[i] + ": " + votesPerCandidate[i] + "
"); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | /* Instead of hardcoding the candidates hash, we now fetch the candidate list from 85 | * the blockchain and populate the array. Once we fetch the candidates, we setup the 86 | * table in the UI with all the candidates and the votes they have received. 87 | */ 88 | function populateCandidates() { 89 | Voting.deployed().then(function(contractInstance) { 90 | contractInstance.allCandidates.call().then(function(candidateArray) { 91 | for(let i=0; i < candidateArray.length; i++) { 92 | /* We store the candidate names as bytes32 on the blockchain. We use the 93 | * handy toUtf8 method to convert from bytes32 to string 94 | */ 95 | candidates[web3.toUtf8(candidateArray[i])] = "candidate-" + i; 96 | } 97 | setupCandidateRows(); 98 | populateCandidateVotes(); 99 | populateTokenData(); 100 | }); 101 | }); 102 | } 103 | 104 | function populateCandidateVotes() { 105 | let candidateNames = Object.keys(candidates); 106 | for (var i = 0; i < candidateNames.length; i++) { 107 | let name = candidateNames[i]; 108 | Voting.deployed().then(function(contractInstance) { 109 | contractInstance.totalVotesFor.call(name).then(function(v) { 110 | $("#" + candidates[name]).html(v.toString()); 111 | }); 112 | }); 113 | } 114 | } 115 | 116 | function setupCandidateRows() { 117 | Object.keys(candidates).forEach(function (candidate) { 118 | $("#candidate-rows").append("" + candidate + ""); 119 | }); 120 | } 121 | 122 | /* Fetch the total tokens, tokens available for sale and the price of 123 | * each token and display in the UI 124 | */ 125 | function populateTokenData() { 126 | Voting.deployed().then(function(contractInstance) { 127 | contractInstance.totalTokens().then(function(v) { 128 | $("#tokens-total").html(v.toString()); 129 | }); 130 | contractInstance.tokensSold.call().then(function(v) { 131 | $("#tokens-sold").html(v.toString()); 132 | }); 133 | contractInstance.tokenPrice().then(function(v) { 134 | tokenPrice = parseFloat(web3.fromWei(v.toString())); 135 | $("#token-cost").html(tokenPrice + " Ether"); 136 | }); 137 | web3.eth.getBalance(contractInstance.address, function(error, result) { 138 | $("#contract-balance").html(web3.fromWei(result.toString()) + " Ether"); 139 | }); 140 | }); 141 | } 142 | 143 | $( document ).ready(function() { 144 | if (typeof web3 !== 'undefined') { 145 | console.warn("Using web3 detected from external source like Metamask") 146 | // Use Mist/MetaMask's provider 147 | window.web3 = new Web3(web3.currentProvider); 148 | } else { 149 | console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask"); 150 | // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) 151 | window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 152 | } 153 | 154 | Voting.setProvider(web3.currentProvider); 155 | populateCandidates(); 156 | 157 | }); 158 | --------------------------------------------------------------------------------