├── .babelrc ├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── DecentralizedSchedule.sol └── Migrations.sol ├── dist └── index.html ├── ethpm.json ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── src ├── App.tsx ├── assets │ └── .gitkeep ├── components │ ├── Button.tsx │ ├── CreateForm.tsx │ ├── CreateSchedule.tsx │ ├── ErrorBox.ts │ ├── EthHeader.ts │ ├── InputElements.ts │ ├── StyleConstants.ts │ ├── Vote.tsx │ └── VotingForm.tsx ├── cryptoutils.ts ├── cryptoutils.tsx ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.tsx ├── model │ ├── EthModel.ts │ └── VotingModel.ts ├── styles.css ├── tsconfig.spec.json └── web3.ts ├── test └── blockchain-schedule.js ├── truffle.init.json ├── truffle.js ├── tsconfig.json ├── tsfmt.json ├── tslint.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/** 2 | build/** 3 | node_modules/** 4 | contracts/** 5 | migrations/1_initial_migration.js 6 | migrations/2_deploy_contracts.js 7 | test/didle.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | yarn-error.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Krzysztof Ciesielski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Decentralized Scheduling 2 | ======================== 3 | 4 | This is an example app demonstrating capabilities of Ethereum and decentralized apps on the blockchain. 5 | Detailed description in [this article](https://softwaremill.com/event-sourcing-on-blockchain/). 6 | 7 | ## Prerequisites 8 | 9 | - npm 10 | - [testrpc](https://github.com/ethereumjs/testrpc) 11 | 12 | ## How to build 13 | 14 | - Call `npm run truffle-compile` to build the smart contract. 15 | - Start testrpc by running `testrpc`. 16 | - Call `truffle deploy` to deploy the smart contract to your local test network. 17 | - Call `npm start` to run the client app. 18 | - Go to `localhost:8080` in your browser. 19 | -------------------------------------------------------------------------------- /contracts/DecentralizedSchedule.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.13; 2 | 3 | contract DecentralizedSchedule { 4 | 5 | // This is a type for a single proposal. 6 | struct Proposal { 7 | bytes32 name; 8 | int128 voteCount; // number of accumulated votes 9 | } 10 | 11 | struct UserVote { 12 | bool voted; 13 | uint proposalIndex; 14 | } 15 | 16 | struct Voting { 17 | string name; 18 | Proposal[] proposals; 19 | mapping(address => UserVote) votes; 20 | } 21 | 22 | // Key here is the unique address generated for each voting, called "signer" 23 | mapping(address => Voting) public votings; 24 | 25 | function voteSummary(address signer) constant returns (string, bytes32[], int128[]) { 26 | return (votings[signer].name, proposalNames(signer), voteCounts(signer)); 27 | } 28 | 29 | function voteCounts(address signer) constant returns (int128[]) { 30 | var ps = votings[signer].proposals; 31 | var arr = new int128[](ps.length); 32 | for (uint i = 0; i < ps.length; i++) { 33 | arr[i] = ps[i].voteCount; 34 | } 35 | return arr; 36 | } 37 | 38 | function proposalNames(address signer) constant returns (bytes32[] names) { 39 | var ps = votings[signer].proposals; 40 | var arr = new bytes32[](ps.length); 41 | for (uint i = 0; i < ps.length; i++) { 42 | arr[i] = ps[i].name; 43 | } 44 | return arr; 45 | } 46 | 47 | function proposalName(address signer, uint8 proposalIndex) constant returns (bytes32 name) { 48 | return votings[signer].proposals[proposalIndex].name; 49 | } 50 | 51 | function isEmpty(string str) constant returns (bool) { 52 | bytes memory tempEmptyStringTest = bytes(str); 53 | return (tempEmptyStringTest.length == 0); 54 | } 55 | 56 | function create(address signer, string name, bytes32[] proposalNames) { 57 | var voting = votings[signer]; 58 | require(isEmpty(voting.name)); 59 | 60 | /* // TODO validations of proposalNames, escape etc. */ 61 | for (uint i = 0; i < proposalNames.length; i++) { 62 | voting.proposals.push(Proposal({ 63 | name: proposalNames[i], 64 | voteCount: 0 65 | })); 66 | } 67 | voting.name = name; 68 | votings[signer] = voting; 69 | } 70 | 71 | event VoteSingle(address voter, address indexed signer, string voterName, uint8 proposal); 72 | 73 | function addressToString(address x) returns (string) { 74 | bytes memory s = new bytes(40); 75 | for (uint i = 0; i < 20; i++) { 76 | byte b = byte(uint8(uint(x) / (2**(8*(19 - i))))); 77 | byte hi = byte(uint8(b) / 16); 78 | byte lo = byte(uint8(b) - 16 * uint8(hi)); 79 | s[2 * i] = char(hi); 80 | s[2* i + 1] = char(lo); 81 | } 82 | return string(s); 83 | } 84 | 85 | function char(byte b) returns (byte c) { 86 | if (b < 10) return byte(uint8(b) + 0x30); 87 | else return byte(uint8(b) + 0x57); 88 | } 89 | 90 | function addressKeccak(address addr) constant returns(bytes32) { 91 | // The "42" here stands for string length of an address 92 | return keccak256("\x19Ethereum Signed Message:\n420x", addressToString(addr)); 93 | } 94 | 95 | function vote(string name, uint8 proposal, bytes32 prefixedSenderHash, bytes32 r, bytes32 s, uint8 v) returns (uint256) { 96 | require(addressKeccak(msg.sender) == prefixedSenderHash); 97 | var signer = ecrecover(prefixedSenderHash, v, r, s); 98 | var voting = votings[signer]; 99 | require(!isEmpty(voting.name)); 100 | require(proposal < voting.proposals.length); 101 | 102 | var prevVote = voting.votes[msg.sender]; 103 | if (prevVote.voted) { 104 | voting.proposals[prevVote.proposalIndex].voteCount -= 1; 105 | } 106 | else { 107 | voting.votes[msg.sender].voted = true; 108 | } 109 | voting.votes[msg.sender].proposalIndex = proposal; 110 | voting.proposals[proposal].voteCount += 1; 111 | 112 | VoteSingle(msg.sender, signer, name, proposal); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.13; 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 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | package_name: "blockchain-schedule", 3 | version: "1.0.0", 4 | description: "Distributed collaborative scheduling on Ethereum blockchain", 5 | authors: [ "Krzysztof Ciesielski" ], 6 | keywords: [ "ethereum", "doodle" ], 7 | license: "MIT" 8 | } 9 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var DecentralizedSchedule = artifacts.require("./DecentralizedSchedule.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(DecentralizedSchedule); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-truffle-ts", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "truffle-compile": "truffle compile", 7 | "start": "webpack-dev-server --inline --content-base dist/ --history-api-fallback", 8 | "lint": "eslint --ext=js --ext=jsx --ignore-path .gitignore .", 9 | "build": "webpack", 10 | "test": "mocha-webpack" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@types/node": "^8.0.7", 15 | "@types/react": "^15.0.33", 16 | "@types/react-dom": "^15.5.1", 17 | "bignumber": "^1.1.0", 18 | "core-js": "^2.4.1", 19 | "ethereumjs-util": "^5.1.2", 20 | "immutability-helper": "^2.3.0", 21 | "lodash": "^4.17.4", 22 | "react": "^15.6.1", 23 | "react-dom": "^15.6.1", 24 | "react-router-dom": "^4.1.1", 25 | "styled-components": "^2.1.1", 26 | "web3": "^0.18.2", 27 | "web3-loader": "^1.1.2" 28 | }, 29 | "devDependencies": { 30 | "@types/lodash": "^4.14.71", 31 | "awesome-typescript-loader": "^3.2.1", 32 | "babel-cli": "^6.22.2", 33 | "babel-core": "^6.22.1", 34 | "babel-eslint": "^6.1.2", 35 | "babel-loader": "^6.2.10", 36 | "babel-plugin-transform-runtime": "^6.22.0", 37 | "babel-preset-env": "^1.1.8", 38 | "babel-preset-es2015": "^6.22.0", 39 | "babel-register": "^6.22.0", 40 | "copy-webpack-plugin": "^4.0.1", 41 | "css-loader": "^0.26.1", 42 | "eslint": "^3.14.0", 43 | "eslint-config-standard": "^6.0.0", 44 | "eslint-plugin-babel": "^4.0.0", 45 | "eslint-plugin-mocha": "^4.8.0", 46 | "eslint-plugin-promise": "^3.0.0", 47 | "eslint-plugin-standard": "^2.0.0", 48 | "ethjs-account": "0.1.0", 49 | "html-webpack-plugin": "^2.28.0", 50 | "style-loader": "^0.13.1", 51 | "truffle": "^3.4.6", 52 | "truffle-contract": "^1.1.6", 53 | "ts-node": "~2.0.0", 54 | "tslint": "~4.4.2", 55 | "typescript": "^2.4.1", 56 | "webpack": "^2.2.1", 57 | "webpack-dev-server": "^2.3.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { injectGlobal } from 'styled-components'; 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link 7 | } from 'react-router-dom'; 8 | 9 | import CreateSchedule from './components/CreateSchedule'; 10 | import Vote from './components/Vote'; 11 | 12 | class App extends React.Component<{}, {}> { 13 | 14 | render() { 15 | injectGlobal` 16 | body { 17 | font-family: 'Open Sans', sans-serif; 18 | margin-top: 10%; 19 | margin-left: 25%; 20 | margin-right: 25%; 21 | } 22 | `; 23 | return ( 24 | 25 |
26 | 27 | 28 |
29 |
30 | ); 31 | } 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/blockchain-schedule/60cbb6013730ba9288334944025dd100524d78ef/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import { DefaultFontSize } from './StyleConstants' 4 | 5 | interface ButtonProps { 6 | className?: string 7 | primary?: boolean 8 | text: string 9 | onClick: (any) => void 10 | } 11 | 12 | const Button = (props: ButtonProps) => 13 | 14 | const StyledButton = styled(Button) ` 15 | background: ${props => props.primary ? '#005dc9' : 'rgb(244, 244, 244)'}; 16 | border: 1px solid; 17 | border-color: ${props => props.primary ? 'rgb(0,78,170)' : 'rgb(204, 204, 204)'}; 18 | border-radius: 4px; 19 | color: ${props => props.primary ? 'rgb(238, 238, 238)' : 'rgb(50, 50, 50)'}; 20 | cursor: pointer; 21 | font-size: ${DefaultFontSize}; 22 | line-height: 18px; 23 | margin-right: 5px; 24 | padding: 0 10px; 25 | ` 26 | 27 | export default StyledButton 28 | -------------------------------------------------------------------------------- /src/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import Button from './Button' 4 | import ErrorBox from './ErrorBox' 5 | import Input, { ShortInput, InputLabel } from './InputElements' 6 | import * as ethjs from 'ethjs-account' 7 | import { withRouter } from 'react-router-dom' 8 | 9 | export interface CreateScheduleState { 10 | name: string, 11 | options: Array, 12 | newOption: string, 13 | formError: boolean, 14 | formErrorMsg: string 15 | } 16 | 17 | export interface CreateScheduleProps { 18 | contract: any 19 | account: any 20 | className?: string 21 | } 22 | 23 | class CreateForm extends React.Component { 24 | 25 | now() { 26 | return new Date().toJSON().slice(0, 10) 27 | } 28 | 29 | public constructor(props: CreateScheduleProps) { 30 | super(props) 31 | let utc: string = this.now() 32 | this.state = { name: "", options: [], newOption: utc, formError: false, formErrorMsg: "" } 33 | this.handleInputChange = this.handleInputChange.bind(this) 34 | this.addNewDate = this.addNewDate.bind(this) 35 | this.validInput = this.validInput.bind(this) 36 | this.createSchedule = this.createSchedule.bind(this) 37 | this.handleNameChange = this.handleNameChange.bind(this) 38 | } 39 | 40 | createSchedule(history: any) { 41 | if (this.state.options.length == 0) { 42 | this.setState({ formError: true, formErrorMsg: "Empty option list not allowed." }) 43 | } else { 44 | const signer = ethjs.generate('892h@fsdf11ks8sk^2h8s8shfs.jk39hsoi@hohskd') 45 | const ballotId: string = signer.address 46 | const key: string = signer.privateKey 47 | 48 | 49 | this.props.contract.deployed() 50 | .then(instance => instance.create(ballotId, this.state.name, this.state.options, { from: this.props.account, gas: 1334400 })) 51 | .then(r => { 52 | console.log("Contract executed") 53 | history.push('/vote?key=' + key + '&b=' + r.receipt.blockNumber) 54 | }) 55 | } 56 | } 57 | 58 | addNewDate() { 59 | if (this.validInput()) { 60 | this.setState({ 61 | options: [...this.state.options, this.state.newOption], 62 | newOption: this.now() 63 | }); 64 | } 65 | } 66 | 67 | validInput(): boolean { 68 | if (this.state.options.some(x => x === this.state.newOption)) { 69 | this.setState({ formError: true, formErrorMsg: "Duplicates not allowed" }) 70 | return false 71 | } else { 72 | this.setState({ formError: false }) 73 | return true 74 | } 75 | } 76 | 77 | handleInputChange(event: React.SyntheticEvent) { 78 | this.setState({ newOption: event.currentTarget.value }) 79 | } 80 | 81 | handleNameChange(event: React.SyntheticEvent) { 82 | this.setState({ name: event.currentTarget.value }) 83 | } 84 | 85 | render() { 86 | const rows = this.state.options.map(opt =>
  • {opt}
  • ) 87 | const CreateButton = withRouter(({ history }) => ( 88 |