├── icon.png ├── .gitignore ├── screenshot.png ├── stake-voice.sublime-project ├── README.md ├── contract.sol ├── index.html ├── style.css └── scripts.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/stake-voice/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | stake-voice.sublime-workspace 3 | 4 | stake-voice.sublime-workspace 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/stake-voice/HEAD/screenshot.png -------------------------------------------------------------------------------- /stake-voice.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stake Voice 2 | Give Ethereum Stakers a voice 3 | 4 | Open this app on [Mist](https://github.com/ethereum/mist): 5 | 6 | [ethereum.github.io/stake-voice/](https://ethereum.github.io/stake-voice/) 7 | 8 | 9 | ![Image](https://raw.githubusercontent.com/ethereum/stake-voice/master/screenshot.png) 10 | -------------------------------------------------------------------------------- /contract.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Based on a contract built by Vlad and Vitalik for Ether signal 3 | If you need a license, refer to WTFPL. 4 | */ 5 | pragma solidity ^0.4.11; 6 | contract EtherVote { 7 | event LogVote(bytes32 indexed proposalHash, bool pro, address addr); 8 | 9 | /// @notice I `pro? agree : disagree` with the statement whose hash is `proposalHash` 10 | /// @param proposalHash hash of the proposal 11 | /// @param pro do you support it or not? 12 | function vote(bytes32 proposalHash, bool pro) { 13 | // Log the vote 14 | LogVote(proposalHash, pro, msg.sender); 15 | } 16 | 17 | // again, no ether 18 | function () { throw; } 19 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stake Voice 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |

Give Stakers a voice

25 | 26 |

27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

35 |
36 | 37 |
38 | This Browser does not support Ethereum apps.
Download Mist 39 |
40 | 41 |
42 | 43 |
44 | 45 | Go! 46 |
47 | 48 | 49 |
50 | 51 |
52 | Click here to add an account on Mist 53 |
54 | 55 | 56 | 65 | 66 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #dedfde; 3 | font-family: 'Source Serif Pro', serif; 4 | background-size: 100%; 5 | } 6 | 7 | .content { 8 | background: #fefefe; 9 | text-align: center; 10 | max-width: 600px; 11 | margin: 90px auto; 12 | padding: 32px; 13 | border-radius: 1px; 14 | box-shadow: rgba(0,0,0,0.2) 0 2px 8px; 15 | } 16 | 17 | button { 18 | font-size: 150%; 19 | font-family: 'Source Serif Pro', serif; 20 | border: none; 21 | color: #FFF; 22 | padding: 10px 40px; 23 | border-radius: 2px; 24 | width: 290px; 25 | margin-top: 10px; 26 | border-bottom: solid #555 3px; 27 | box-shadow: #ddd 0 2px 2px; 28 | outline: none; 29 | } 30 | 31 | button:active, 32 | button.pressed { 33 | border-bottom: none; 34 | box-shadow: inset rgba(0,0,0,0.5) 0 2px 2px; 35 | position: relative; 36 | top: 3px; 37 | } 38 | 39 | #results #support , 40 | button#vote-support { 41 | background: green; 42 | border-bottom-color: darkgreen; 43 | } 44 | 45 | #results #opposition, 46 | button#vote-against { 47 | background: red; 48 | border-bottom-color: darkred; 49 | } 50 | 51 | button#see-results { 52 | background: #666; 53 | width: 584px; 54 | transition: opacity 1s ease-in-out; 55 | } 56 | 57 | button.pressed { 58 | 59 | } 60 | 61 | h2 { 62 | opacity: 0.5; 63 | } 64 | 65 | .write { 66 | position: relative; 67 | background: #eee; 68 | padding: 20px 10px; 69 | border-radius: 2px; 70 | margin: 0 -32px -32px; 71 | } 72 | 73 | #new-proposal { 74 | font-family: 'Source Serif Pro', serif; 75 | font-size: 150%; 76 | width: 600px; 77 | padding: 5px 10px; 78 | border-radius: 3px; 79 | border: solid 2px #ddd; 80 | } 81 | 82 | #new-proposal-link { 83 | position: absolute; 84 | top: 24px; 85 | right: 24px; 86 | background: #333; 87 | color: #FFF; 88 | padding: 8px 15px; 89 | text-decoration: none; 90 | font-weight: 600; 91 | border-radius: 2px; 92 | display: none; 93 | } 94 | 95 | #status { 96 | position: fixed; 97 | bottom: 32px; 98 | left: 0; 99 | right: 0; 100 | font-size: 150%; 101 | opacity: 0.5; 102 | text-align: center; 103 | } 104 | 105 | #results { 106 | opacity: 0; 107 | transition: opacity 1s ease-in-out; 108 | } 109 | #results h3 { 110 | display: inline-block; 111 | width: 50%; 112 | padding: 20px 0; 113 | color: #FFF; 114 | width: 1%; 115 | max-width: 99.5% !important; 116 | min-width: 0.5% !important; 117 | overflow: hidden; 118 | text-overflow: clip; 119 | white-space: nowrap; 120 | transition: width 1s ease-in-out, min-width 1s ease-in-out, max-width 1s ease-in-out; 121 | text-indent: 8px; 122 | } 123 | 124 | #results:hover h3 { 125 | max-width: 80% !important; 126 | } 127 | 128 | #results:hover h3:hover { 129 | min-width: 20% !important; 130 | max-width: 99.5% !important; 131 | } 132 | 133 | #message { 134 | display: none; 135 | padding: 30px; 136 | font-size: 150%; 137 | font-style: italic; 138 | } 139 | 140 | #add-account { 141 | display: none; 142 | background: #333; 143 | color: #FFF; 144 | position: fixed; 145 | top: 90px; 146 | right: 32px; 147 | padding: 20px 30px; 148 | box-shadow: rgba(0,0,0,0.2) 0 2px 8px; 149 | } 150 | 151 | #add-account:before { 152 | display: block; 153 | background: #333; 154 | transform: rotate(45deg); 155 | top: -9px; 156 | width: 20px; 157 | height: 20px; 158 | content: " "; 159 | position: absolute; 160 | left: 250px; 161 | } 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | // Set some initial variables 2 | 3 | var ethervote, ethervoteContract, proposalHash, totalVotes, proposal, totalPro, totalAgainst; 4 | var voteMap = {}; 5 | 6 | var contractAddress = '0x8a57d2708d1f228dac2f7934f5311cd2a0a1cda4'; 7 | var contractAddressTestnet = '0x4ad62d4aaec13098832b1be635fc01581d97325c'; // Rinkeby 8 | // Ropsten: 0x47ab800a75990b0bd5bb4a54cfbec777972c973c 9 | 10 | var startingBlock = 4000000; // on mainnet 11 | 12 | var contractABI = [{"constant":false,"inputs":[{"name":"proposalHash","type":"bytes32"},{"name":"pro","type":"bool"}],"name":"vote","outputs":[],"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalHash","type":"bytes32"},{"indexed":false,"name":"pro","type":"bool"},{"indexed":false,"name":"addr","type":"address"}],"name":"LogVote","type":"event"}]; 13 | var history = []; 14 | 15 | function init() { 16 | // Get parameters and set up the basic structure 17 | proposal = decodeURI(getParameterByName('proposal')); 18 | document.getElementById('proposal').textContent = proposal; 19 | 20 | // Add event listeners 21 | document.getElementById('see-results').addEventListener('click', function(){ 22 | document.getElementById("results").style.opacity = "1"; 23 | document.getElementById("see-results").style.opacity = "0"; 24 | } , false); 25 | document.getElementById('vote-support').addEventListener('click', function(){ vote(true);}, false); 26 | document.getElementById('vote-against').addEventListener('click', function(){ vote(false);}, false); 27 | var newProposalInput = document.getElementById('new-proposal'); 28 | newProposalInput.addEventListener('keypress', function() { 29 | document.getElementById("new-proposal-link").style.display = "block"; 30 | }); 31 | newProposalInput.addEventListener('blur', newProposal); 32 | 33 | 34 | // Checks Web3 support 35 | if(typeof web3 !== 'undefined' && typeof Web3 !== 'undefined') { 36 | // If there's a web3 library loaded, then make your own web3 37 | web3 = new Web3(web3.currentProvider); 38 | } else if (typeof Web3 !== 'undefined') { 39 | // If there isn't then set a provider 40 | web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 41 | } else if(typeof web3 == 'undefined') { 42 | // If there is neither then this isn't an ethereum browser 43 | document.getElementById("results").style.display = "none"; 44 | document.getElementById("see-results").style.display = "none"; 45 | document.getElementById("vote-support").style.display = "none"; 46 | document.getElementById("vote-against").style.display = "none"; 47 | document.getElementById("subtitle").style.display = "none"; 48 | document.getElementById("proposal").textContent = "Give Stakers a Voice"; 49 | var message = document.getElementById("message"); 50 | message.style.display = "block"; 51 | return; 52 | } 53 | 54 | // Check if there are available accounts 55 | web3.eth.getAccounts(function(e,accounts){ 56 | // show the floating baloon 57 | if (e || !accounts || accounts.length == 0) { 58 | document.getElementById("add-account").style.display = "block"; 59 | } 60 | }); 61 | 62 | 63 | 64 | // Get the proposal 65 | proposalHash = web3.sha3(proposal); 66 | document.body.style.background = "#" + proposalHash.substr(2,6); 67 | 68 | if (typeof proposal == 'undefined' || proposal == 'null' || proposal == '') { 69 | // No Proposals are set 70 | document.getElementById("results").style.display = "none"; 71 | document.getElementById("see-results").style.display = "none"; 72 | document.getElementById("vote-support").style.display = "none"; 73 | document.getElementById("vote-against").style.display = "none"; 74 | document.getElementById("subtitle").style.display = "none"; 75 | document.getElementById("proposal").textContent = "Give Stakers a Voice"; 76 | var message = document.getElementById("message"); 77 | message.style.display = "block"; 78 | message.textContent = "This tool will enable anyone to create any statement that ethereum token holders can voice their support or opposition to. Statements are not binding and represent only the opinion of those who support it."; 79 | } else { 80 | // If proposal is valid, start watching the chain 81 | web3.eth.filter('latest').watch(function(e, res){ 82 | console.log('web3.filter', e, res) 83 | if(!e) { 84 | console.log('Block arrived ', res); 85 | document.getElementById('status').textContent = 'Calculating votes...'; 86 | calculateVotes(); 87 | } 88 | }); 89 | } 90 | 91 | // Load the contract 92 | web3.eth.getCode(contractAddress, function(e, r) { 93 | if (!e) { 94 | // if bytecode is small, then try switching networks 95 | if (r.length < 3) { 96 | contractAddress = contractAddressTestnet; 97 | startingBlock = 500000; 98 | } 99 | 100 | web3.eth.getCode(contractAddress, function(e, r) { 101 | if (!e) { 102 | 103 | }}) 104 | // Load the contract 105 | ethervoteContract = web3.eth.contract(contractABI); 106 | ethervote = ethervoteContract.at(contractAddress); 107 | 108 | // Watch Votes 109 | if (proposal && proposal.length > 0 && proposal != 'null') 110 | watchVotes(); 111 | } 112 | }) 113 | 114 | 115 | 116 | // Build Mist Menu 117 | // Add proposal to history 118 | if (typeof(Storage) !== "undefined" && typeof(mist) !== "undefined") { 119 | // Code for localStorage/sessionStorage. 120 | var propHistory = localStorage.propHistory ? localStorage.propHistory.split(',') : []; 121 | if (proposal && proposal.length > 0 && propHistory.indexOf(proposal)<0) 122 | propHistory.unshift(proposal); 123 | 124 | propHistory = propHistory.slice(0, 10); 125 | localStorage.setItem('propHistory', propHistory.join(',')); 126 | 127 | // mist.menu.clear(); 128 | mist.menu.add( 'main' ,{ 129 | position: 0, 130 | name: 'Main Page', 131 | selected: typeof proposal == 'undefined' || proposal == 'null' 132 | }, function(){ 133 | window.location.search = ''; 134 | }); 135 | 136 | var n = 1; 137 | for (item of propHistory) { 138 | if (item.length > 0 && item != 'null') { 139 | mist.menu.add( item ,{ 140 | name: item, 141 | position: n++, 142 | selected: item == proposal 143 | }, function(){ 144 | window.location.search = '?proposal=' + encodeURI(this.name); 145 | }); 146 | } 147 | } 148 | 149 | } 150 | 151 | } 152 | 153 | 154 | function watchVotes() { 155 | // Set the texts and variables 156 | document.getElementById('status').textContent = 'Calculating votes...'; 157 | 158 | setTimeout(function(){ 159 | // If the app doesn't respond after a timeout it probably has no votes 160 | document.getElementById('status').textContent = ""; 161 | }, 3000); 162 | 163 | 164 | // LogVote is an event on the contract. Read all since block 1 million 165 | var logVotes = ethervote.LogVote({proposalHash: proposalHash}, {fromBlock: startingBlock}); 166 | 167 | // Wait for the events to be loaded 168 | logVotes.watch(function(error, res){ 169 | console.log('logVotes Watch', error, res); 170 | 171 | // Each vote will execute this function 172 | if (!error) { 173 | 174 | web3.eth.getBalance(res.args.addr, function(err, balanceInWei){ 175 | // Get the current balance of a voter 176 | var bal = Number(web3.fromWei(balanceInWei, "finney")); 177 | 178 | voteMap[res.args.addr] = {balance: bal, support: res.args.pro}; 179 | 180 | // Check if the current owner has already voted and show that on the interface 181 | web3.eth.getAccounts(function(e,accounts){ 182 | if (!e && accounts && accounts[0] == res.args.addr) { 183 | if (res.args.pro) { 184 | document.getElementById('vote-support').classList.add("pressed"); 185 | document.getElementById('vote-against').classList.remove("pressed"); 186 | } else { 187 | document.getElementById('vote-support').classList.remove("pressed"); 188 | document.getElementById('vote-against').classList.add("pressed"); 189 | } 190 | } 191 | }); 192 | calculateVotes(); 193 | 194 | }) 195 | 196 | } 197 | }) 198 | } 199 | 200 | function convertToString(vote, total){ 201 | // how many 0's are we dealing with 202 | var magnitude = Math.floor(Math.log10(total)); 203 | 204 | // Select the right unit 205 | if (magnitude <= 3) { 206 | return Math.round(vote*10)/10 + " finney"; 207 | } else if (magnitude < 6) { 208 | return Math.round(vote/10)/100 + " ether"; 209 | } else if (magnitude < 9) { 210 | return Math.round(vote/10000)/100 + "k ether"; 211 | } else { 212 | return Math.round(vote/10000000)/100 + " million ether"; 213 | } 214 | 215 | } 216 | 217 | function calculateVotes() { 218 | 219 | for (var a in voteMap) { 220 | // call the function asynchronously 221 | web3.eth.getBalance(a, function(e,r) { 222 | voteMap[a].balance = Number(web3.fromWei(r, 'finney')); 223 | updateTotals() 224 | }); 225 | }; 226 | 227 | // End the calculation 228 | document.getElementById("message").style.display = "none"; 229 | 230 | setTimeout(function(){ 231 | // If the app doesn't respond after a timeout it probably has no votes 232 | document.getElementById('status').textContent = ""; 233 | 234 | if (!(totalVotes > 0)){ 235 | document.getElementById("results").style.display = "none"; 236 | var message = document.getElementById("message"); 237 | message.textContent = "No votes yet. Vote now!"; 238 | message.style.display = "block"; 239 | } 240 | }, 2000); 241 | } 242 | 243 | function updateTotals() { 244 | totalPro = 0; 245 | totalAgainst = 0; 246 | totalVotes = 0; 247 | 248 | for (var acc in voteMap) { 249 | if (voteMap[acc].support) 250 | totalPro += parseFloat(voteMap[acc].balance); 251 | else 252 | totalAgainst += parseFloat(voteMap[acc].balance); 253 | } 254 | 255 | totalVotes = totalPro + totalAgainst; 256 | 257 | // Show a colored bar with the result 258 | document.getElementById("results").style.display = "block"; 259 | var proResult = document.getElementById('support'); 260 | proResult.textContent = convertToString(totalPro, totalVotes); 261 | proResult.style.width = Math.round(totalPro*100/totalVotes) + "%"; 262 | var againstResult = document.getElementById('opposition'); 263 | againstResult.textContent = convertToString(totalAgainst, totalVotes); 264 | againstResult.style.width = Math.round(totalAgainst*100/totalVotes) + "%"; 265 | 266 | if (totalVotes>0) 267 | mist.menu.update( proposal ,{ badge: Math.round(totalPro*100/totalVotes) + "%" }); 268 | 269 | } 270 | 271 | function vote(support) { 272 | 273 | web3.eth.getAccounts(function(e,accounts){ 274 | // Check if there are accounts available 275 | if (!e && accounts && accounts.length > 0) { 276 | // Create a dialog requesting the transaction 277 | ethervote.vote(proposalHash, support, {from: accounts[0]}, function(err, res) { 278 | console.log('voted!', err, res); 279 | }) 280 | document.getElementById('status').textContent = 'Waiting for new block...'; 281 | 282 | } else { 283 | mist.requestAccount(function(e, account) { 284 | if(!e) { 285 | // Create a dialog requesting the transaction 286 | ethervote.vote(proposalHash, support, {from: account.toLowerCase()}, function(err, res) { 287 | console.log('voted!', err, res); 288 | }) 289 | document.getElementById('status').textContent = 'Waiting for new block...'; 290 | } 291 | }); 292 | } 293 | }) 294 | 295 | document.getElementById("results").style.opacity = "1"; 296 | document.getElementById("see-results").style.opacity = "0"; 297 | } 298 | 299 | 300 | function getParameterByName(name, url) { 301 | if (!url) url = window.location.href; 302 | name = name.replace(/[\[\]]/g, "\\$&"); 303 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 304 | results = regex.exec(url); 305 | if (!results) return null; 306 | if (!results[2]) return ''; 307 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 308 | } 309 | 310 | function newProposal() { 311 | // When typing a new proposal, generate new dinamic urls 312 | var newProposal = document.getElementById('new-proposal'); 313 | var newProposalLink = document.getElementById('new-proposal-link'); 314 | newProposalLink.href = '?proposal=' + encodeURI(newProposal.value); 315 | } 316 | --------------------------------------------------------------------------------