├── .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 |