├── src ├── redux │ ├── actions │ │ ├── .gitkeep │ │ ├── alert-actions.js │ │ ├── abi │ │ │ ├── positionRegistryAbi.js │ │ │ └── etherSignalAbi.js │ │ ├── utils │ │ │ └── getSignalPerBlock.js │ │ ├── connection-actions.js │ │ └── position-actions.js │ └── reducers │ │ ├── index.js │ │ ├── alert-reducer.js │ │ ├── connection-reducer.js │ │ └── position-reducer.js ├── styles │ ├── atoms │ │ ├── .gitkeep │ │ └── LoadingAnimation.css │ ├── ecosystems │ │ ├── .gitkeep │ │ ├── Alerts.css │ │ └── PositionSubmitter.css │ ├── environments │ │ ├── .gitkeep │ │ ├── Home.css │ │ └── Frame.css │ ├── molecules │ │ ├── .gitkeep │ │ └── PositionListItem.css │ └── organisms │ │ ├── .gitkeep │ │ ├── NetworkStatus.css │ │ ├── Alert.css │ │ ├── AccountSelector.css │ │ ├── PositionFilter.css │ │ └── PositionPagination.css ├── components │ ├── atoms │ │ ├── .gitkeep │ │ └── LoadingAnimation.js │ ├── molecules │ │ ├── .gitkeep │ │ ├── PositionDepositInput.js │ │ └── PositionListItem.js │ ├── organisms │ │ ├── .gitkeep │ │ ├── Alert.js │ │ ├── PositionList.js │ │ ├── AccountSelector.js │ │ ├── PositionPagination.js │ │ ├── SignalChart.js │ │ ├── PositionDepositModal.js │ │ ├── NetworkStatus.js │ │ ├── PositionSubmitterForm.js │ │ └── PositionFilter.js │ ├── ecosystems │ │ ├── .gitkeep │ │ ├── Alerts.js │ │ ├── PositionSubmitter.js │ │ ├── PositionSubmitterModal.js │ │ └── Positions.js │ └── environments │ │ ├── .gitkeep │ │ ├── About.js │ │ ├── Home.js │ │ ├── Frame.js │ │ └── CliQuickstart.js ├── images │ ├── ajax.gif │ └── logo.svg ├── index.css └── index.js ├── config ├── flow │ ├── css.js.flow │ └── file.js.flow ├── babel.dev.js ├── babel.prod.js ├── eslint.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── favicon.ico ├── .eslintrc.json ├── cli ├── test1.js ├── readmev2.txt └── ethersignal2.js ├── README.md ├── index.html ├── scripts ├── openChrome.applescript ├── build.js └── start.js ├── contracts └── ethersignal2.sol └── package.json /src/redux/actions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/atoms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/atoms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/molecules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/organisms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/ecosystems/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/environments/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/environments/Home.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/molecules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/organisms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/flow/css.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | -------------------------------------------------------------------------------- /src/components/ecosystems/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/environments/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | build 3 | .npm-debug.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /config/flow/file.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string; 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulcanize/ethersignal/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/images/ajax.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulcanize/ethersignal/HEAD/src/images/ajax.gif -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/atoms/LoadingAnimation.css: -------------------------------------------------------------------------------- 1 | .loading-animation { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/organisms/NetworkStatus.css: -------------------------------------------------------------------------------- 1 | samp.network-status { 2 | font-size: .8em; 3 | color: #666; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/organisms/Alert.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | 3 | } 4 | 5 | .alert:last-of-type { 6 | margin-bottom: 0; 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "trails/react", 3 | "parserOptions": { 4 | "sourceType": "module" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/ecosystems/Alerts.css: -------------------------------------------------------------------------------- 1 | .alerts { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | width: 100%; 6 | padding: .5em; 7 | z-index: 5; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/organisms/AccountSelector.css: -------------------------------------------------------------------------------- 1 | .account-selector { 2 | margin-bottom: .5em; 3 | } 4 | 5 | .account-selector .control-label { 6 | margin-right: .5em; 7 | } 8 | -------------------------------------------------------------------------------- /cli/test1.js: -------------------------------------------------------------------------------- 1 | function test1_voteSpam() { 2 | for (i = 0; i < 100; i++) { 3 | ethersignal.setSignal(0, ((i % 2) == 0) ? true : false, {from: web3.eth.accounts[0], gas: 300000}); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/ecosystems/PositionSubmitter.css: -------------------------------------------------------------------------------- 1 | .position-submitter { 2 | text-align: center; 3 | } 4 | 5 | .position-submitter-logo { 6 | margin-right: .125em; 7 | max-height: 60px; 8 | } 9 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import positions from './position-reducer' 4 | import connection from './connection-reducer' 5 | import alerts from './alert-reducer' 6 | 7 | export default combineReducers({ 8 | positions, 9 | connection, 10 | alerts 11 | }) 12 | -------------------------------------------------------------------------------- /src/styles/organisms/PositionFilter.css: -------------------------------------------------------------------------------- 1 | .position-filter { 2 | display: flex; 3 | align-items: center; 4 | margin: 0 -.5em; 5 | } 6 | 7 | .position-filter > div { 8 | margin: 0 .5em; 9 | } 10 | 11 | .position-filter .control-label { 12 | margin-right: .5em; 13 | } 14 | 15 | .position-filter .icon { 16 | margin-right: .5em; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/environments/Frame.css: -------------------------------------------------------------------------------- 1 | .navbar .navbar-brand { 2 | 3 | } 4 | 5 | .app-header-logo { 6 | max-width: 17px; 7 | } 8 | 9 | .navbar .navbar-brand > img { 10 | display: inline; 11 | margin-right: .125em; 12 | } 13 | 14 | .navbar .navbar-brand a { 15 | color: #777; 16 | } 17 | 18 | .navbar .navbar-brand a:hover { 19 | color: #555; 20 | text-decoration: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/organisms/PositionPagination.css: -------------------------------------------------------------------------------- 1 | .position-pagination { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin: 0 -1em; 6 | } 7 | 8 | .position-pagination > div { 9 | margin: 0 1em; 10 | } 11 | 12 | .position-pagination ul.pagination { 13 | margin: 0; 14 | } 15 | 16 | .position-pagination .control-label { 17 | margin-right: .5em; 18 | } 19 | -------------------------------------------------------------------------------- /config/babel.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: true, 3 | presets: [ 4 | 'babel-preset-es2015', 5 | 'babel-preset-es2016', 6 | 'babel-preset-react' 7 | ].map(require.resolve), 8 | plugins: [ 9 | 'babel-plugin-syntax-trailing-function-commas', 10 | 'babel-plugin-transform-class-properties', 11 | 'babel-plugin-transform-object-rest-spread' 12 | ].map(require.resolve) 13 | }; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ether Signal 2 | 3 | A React implementation of https://github.com/vulcanize/ethersignal 4 | 5 | ## Setting up development server 6 | From terminal: 7 | ```js 8 | git clone git@github.com:langateam/ethersignal.git && cd ethersignal 9 | npm install 10 | npm start 11 | ``` 12 | Open localhost:8080 in MIST. 13 | 14 | ### Contract on TestNet 15 | http://testnet.etherscan.io/address/0x9e75993a7a9b9f92a1978bcc15c30cbcb967bc81#code 16 | -------------------------------------------------------------------------------- /config/babel.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'babel-preset-es2015', 4 | 'babel-preset-es2016', 5 | 'babel-preset-react' 6 | ].map(require.resolve), 7 | plugins: [ 8 | 'babel-plugin-syntax-trailing-function-commas', 9 | 'babel-plugin-transform-class-properties', 10 | 'babel-plugin-transform-object-rest-spread', 11 | 'babel-plugin-transform-react-constant-elements' 12 | ].map(require.resolve) 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/atoms/LoadingAnimation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import loading from './../../images/ajax.gif' 3 | 4 | import './../../styles/atoms/LoadingAnimation.css' 5 | 6 | class LoadingAnimation extends Component { 7 | 8 | render() { 9 | return ( 10 |
11 | loading 12 |
13 | ) 14 | } 15 | 16 | } 17 | 18 | export default LoadingAnimation 19 | -------------------------------------------------------------------------------- /src/components/environments/About.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class About extends Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

About

9 |

EtherSignal intends to allow the Ethereum community to signal 10 | on their positions with their ether.

11 |

Practically it allows for the creation and discovery of positions, 12 | along with the monitoring of and participating in signaling on said 13 | positions.

14 |
15 | ) 16 | } 17 | 18 | } 19 | 20 | About.propTypes = {} 21 | 22 | export default About 23 | -------------------------------------------------------------------------------- /src/components/organisms/Alert.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import './../../styles/organisms/Alert.css' 4 | 5 | import { 6 | Alert as BsAlert 7 | } from 'react-bootstrap' 8 | 9 | class Alert extends Component { 10 | 11 | render() { 12 | return ( 13 | 16 | {this.props.text} 17 | 18 | ) 19 | } 20 | 21 | } 22 | 23 | Alert.propTypes = { 24 | onClick: PropTypes.func, 25 | severity: PropTypes.string, 26 | text: PropTypes.string 27 | } 28 | 29 | export default Alert 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EtherSignal 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/redux/reducers/alert-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_ALERT, 3 | REMOVE_ALERT, 4 | REMOVE_TIMED_ALERT 5 | } from './../actions/alert-actions' 6 | 7 | const initialState = [] 8 | 9 | export default function(state = initialState, action) { 10 | 11 | switch (action.type) { 12 | 13 | case ADD_ALERT: 14 | return [ 15 | ...state, 16 | { 17 | text: action.text, 18 | severity: action.severity, 19 | id: state.length 20 | } 21 | ] 22 | 23 | case REMOVE_ALERT: 24 | return state.filter(item => item.id !== action.id) 25 | 26 | case REMOVE_TIMED_ALERT: 27 | return state.filter(item => { 28 | return item.text !== action.text && 29 | item.severity !== action.severity 30 | }) 31 | 32 | default: 33 | return state 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/redux/actions/alert-actions.js: -------------------------------------------------------------------------------- 1 | export const ADD_ALERT = 'ADD_ALERT' 2 | export const REMOVE_ALERT = 'REMOVE_ALERT' 3 | 4 | 5 | export function addAlert(text, severity) { 6 | return { 7 | type: ADD_ALERT, 8 | text, 9 | severity 10 | } 11 | } 12 | 13 | export function removeAlert(id) { 14 | return { 15 | type: REMOVE_ALERT, 16 | id 17 | } 18 | } 19 | 20 | export const REMOVE_TIMED_ALERT = 'REMOVE_TIMED_ALERT' 21 | 22 | export function removeTimedAlert(text, severity) { 23 | return { 24 | type: REMOVE_TIMED_ALERT, 25 | text, 26 | severity 27 | } 28 | } 29 | 30 | export function addTimedAlert(text, severity) { 31 | return dispatch => { 32 | dispatch(addAlert(text, severity)) 33 | setTimeout(() => { 34 | dispatch(removeTimedAlert(text, severity)) 35 | }, 3000) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/redux/actions/abi/positionRegistryAbi.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | 'constant': false, 4 | 'inputs': [{ 5 | 'name': 'title', 6 | 'type': 'string' 7 | }, { 8 | 'name': 'text', 9 | 'type': 'string' 10 | }], 11 | 'name': 'registerPosition', 12 | 'outputs': [], 13 | 'type': 'function' 14 | }, { 15 | 'anonymous': false, 16 | 'inputs': [{ 17 | 'indexed': true, 18 | 'name': 'regAddr', 19 | 'type': 'address' 20 | }, { 21 | 'indexed': true, 22 | 'name': 'sigAddr', 23 | 'type': 'address' 24 | }, { 25 | 'indexed': false, 26 | 'name': 'title', 27 | 'type': 'string' 28 | }, { 29 | 'indexed': false, 30 | 'name': 'text', 31 | 'type': 'string' 32 | }], 33 | 'name': 'LogPosition', 34 | 'type': 'event' 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /config/eslint.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/airbnb/javascript but less opinionated. 2 | 3 | // We use eslint-loader so even warnings are very visibile. 4 | // This is why we only use "WARNING" level for potential errors, 5 | // and we don't use "ERROR" level at all. 6 | 7 | // In the future, we might create a separate list of rules for production. 8 | // It would probably be more strict. 9 | 10 | var WARNING = 1; 11 | 12 | module.exports = { 13 | root: true, 14 | 15 | parser: 'babel-eslint', 16 | 17 | extends: 'trails/react', 18 | parserOptions: { 19 | sourceType: 'module', 20 | }, 21 | 22 | rules: { 23 | 'new-cap': [ 'error', { 'capIsNew': false }] 24 | }, 25 | 26 | settings: { 27 | 'import/ignore': [ 28 | 'node_modules', 29 | '\\.(json|css|jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$', 30 | ], 31 | 'import/extensions': ['.js'], 32 | 'import/resolver': { 33 | node: { 34 | extensions: ['.js', '.json'] 35 | } 36 | } 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /scripts/openChrome.applescript: -------------------------------------------------------------------------------- 1 | on run argv 2 | set theURL to item 1 of argv 3 | 4 | tell application "Chrome" 5 | 6 | if (count every window) = 0 then 7 | make new window 8 | end if 9 | 10 | -- Find a tab currently running the debugger 11 | set found to false 12 | set theTabIndex to -1 13 | repeat with theWindow in every window 14 | set theTabIndex to 0 15 | repeat with theTab in every tab of theWindow 16 | set theTabIndex to theTabIndex + 1 17 | if theTab's URL is theURL then 18 | set found to true 19 | exit repeat 20 | end if 21 | end repeat 22 | 23 | if found then 24 | exit repeat 25 | end if 26 | end repeat 27 | 28 | if found then 29 | tell theTab to reload 30 | set index of theWindow to 1 31 | set theWindow's active tab index to theTabIndex 32 | else 33 | tell window 1 34 | activate 35 | make new tab with properties {URL:theURL} 36 | end tell 37 | end if 38 | end tell 39 | end run 40 | -------------------------------------------------------------------------------- /src/components/ecosystems/Alerts.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { connect } from 'react-redux' 4 | import Alert from './../organisms/Alert' 5 | import './../../styles/ecosystems/Alerts.css' 6 | 7 | import { 8 | removeAlert 9 | } from './../../redux/actions/alert-actions' 10 | 11 | class Alerts extends Component { 12 | 13 | dismiss(id) { 14 | this.props.dispatch(removeAlert(id)) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | { 21 | this.props.alerts.map(alert => { 22 | return ( 23 | 27 | ) 28 | }) 29 | } 30 |
31 | ) 32 | } 33 | 34 | } 35 | 36 | Alerts.propTypes = { 37 | alerts: PropTypes.array, 38 | dispatch: PropTypes.func 39 | } 40 | 41 | export default connect( 42 | state => ({ 43 | alerts: state.alerts 44 | }) 45 | )(Alerts) 46 | -------------------------------------------------------------------------------- /src/components/environments/Home.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import PositionSubmitter from './../ecosystems/PositionSubmitter' 6 | import Positions from './../ecosystems/Positions' 7 | 8 | import './../../styles/environments/Home.css' 9 | 10 | class Home extends Component { 11 | 12 | render() { 13 | return ( 14 |
15 | 19 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | Home.propTypes = { 29 | dispatch: PropTypes.func, 30 | positions: PropTypes.object, 31 | connection: PropTypes.object 32 | } 33 | 34 | export default connect( 35 | state => ({ 36 | positions: state.positions, 37 | connection: state.connection 38 | }) 39 | )(Home) 40 | -------------------------------------------------------------------------------- /src/redux/actions/abi/etherSignalAbi.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | 'constant': false, 4 | 'inputs': [{ 5 | 'name': 'amount', 6 | 'type': 'uint256' 7 | }], 8 | 'name': 'withdraw', 9 | 'outputs': [], 10 | 'type': 'function' 11 | }, { 12 | 'constant': false, 13 | 'inputs': [{ 14 | 'name': 'pro', 15 | 'type': 'bool' 16 | }], 17 | 'name': 'setSignal', 18 | 'outputs': [], 19 | 'type': 'function' 20 | }, { 21 | 'constant': false, 22 | 'inputs': [], 23 | 'name': 'endSignal', 24 | 'outputs': [], 25 | 'type': 'function' 26 | }, { 27 | 'inputs': [{ 28 | 'name': 'rAddr', 29 | 'type': 'address' 30 | }], 31 | 'type': 'constructor' 32 | }, { 33 | 'anonymous': false, 34 | 'inputs': [{ 35 | 'indexed': false, 36 | 'name': 'pro', 37 | 'type': 'bool' 38 | }, { 39 | 'indexed': false, 40 | 'name': 'addr', 41 | 'type': 'address' 42 | }], 43 | 'name': 'LogSignal', 44 | 'type': 'event' 45 | }, { 46 | 'anonymous': false, 47 | 'inputs': [], 48 | 'name': 'EndSignal', 49 | 'type': 'event' 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | var path = require('path'); 4 | var rimrafSync = require('rimraf').sync; 5 | var webpack = require('webpack'); 6 | var config = require('../config/webpack.config.prod'); 7 | 8 | var isInNodeModules = 'node_modules' === 9 | path.basename(path.resolve(path.join(__dirname, '..', '..'))); 10 | var relative = isInNodeModules ? '../..' : '.'; 11 | rimrafSync(relative + '/build'); 12 | 13 | webpack(config).run(function(err, stats) { 14 | if (err) { 15 | console.error('Failed to create a production build. Reason:'); 16 | console.error(err.message || err); 17 | process.exit(1); 18 | } 19 | 20 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 21 | console.log('Successfully generated a bundle in the build folder!'); 22 | console.log(); 23 | console.log('You can now serve it with any static server, for example:'); 24 | console.log(' cd build'); 25 | console.log(' npm install -g http-server'); 26 | console.log(' hs'); 27 | console.log(' ' + openCommand + ' http://localhost:8080'); 28 | console.log(); 29 | console.log('The bundle is optimized and ready to be deployed to production.'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/organisms/PositionList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import PositionListItem from './../molecules/PositionListItem' 4 | import LoadingAnimation from './../atoms/LoadingAnimation' 5 | 6 | import { 7 | ListGroup 8 | } from 'react-bootstrap' 9 | 10 | class PositionList extends Component { 11 | 12 | render() { 13 | 14 | return ( 15 | 16 | { 17 | this.props.fetching && 18 | this.props.items.length === 0 && 19 | 20 | } 21 | { 22 | this.props.items.map((position, index) => { 23 | return ( 24 | 29 | ) 30 | }) 31 | } 32 | 33 | ) 34 | } 35 | 36 | } 37 | 38 | PositionList.propTypes = { 39 | fetching: PropTypes.bool, 40 | dispatch: PropTypes.func, 41 | items: PropTypes.array, 42 | account: PropTypes.string 43 | } 44 | 45 | export default PositionList 46 | -------------------------------------------------------------------------------- /contracts/ethersignal2.sol: -------------------------------------------------------------------------------- 1 | contract EtherSignal { 2 | address regAddr; 3 | 4 | function EtherSignal(address rAddr) { 5 | regAddr = rAddr; 6 | } 7 | 8 | event LogSignal(bool pro, address addr); 9 | event EndSignal(); 10 | 11 | function setSignal(bool pro) { 12 | LogSignal(pro, msg.sender); 13 | } 14 | 15 | function endSignal() { 16 | if (msg.sender != regAddr) { throw; } 17 | EndSignal(); 18 | suicide(msg.sender); 19 | } 20 | 21 | function withdraw(uint amount) { 22 | if (msg.sender != regAddr) { throw; } 23 | if (amount > this.balance) { throw; } 24 | if (!msg.sender.send(amount)) { throw; } 25 | } 26 | 27 | function () { 28 | if (msg.sender != regAddr) { throw; } 29 | // accept deposit only from the address which registered the position 30 | } 31 | } 32 | 33 | contract PositionRegistry { 34 | event LogPosition(address indexed regAddr, address indexed sigAddr, string title, string text); 35 | 36 | function registerPosition(string title, string text) { 37 | address conAddr = new EtherSignal(msg.sender); 38 | if (conAddr == 0x0) { throw; } 39 | LogPosition(msg.sender, conAddr, title, text); 40 | } 41 | 42 | function () { 43 | throw; // do not accept ether 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/styles/molecules/PositionListItem.css: -------------------------------------------------------------------------------- 1 | .position-list-item { 2 | display: flex; 3 | align-items: center; 4 | margin: 1em -1em; 5 | } 6 | 7 | .position-list-item > div { 8 | margin: 0 1em; 9 | } 10 | 11 | .position-list-item .statistics { 12 | width: 40%; 13 | } 14 | 15 | .position-list-item .statistics label { 16 | margin-bottom: 0; 17 | } 18 | 19 | .position-list-item .statistics > div { 20 | margin-bottom: 1em; 21 | } 22 | 23 | .position-list-item .deposit { 24 | display: flex; 25 | } 26 | 27 | .position-list-item .current-deposit { 28 | margin-right: .5em; 29 | } 30 | 31 | .voting { 32 | margin-bottom: 1em; 33 | display: flex; 34 | flex-direction: column; 35 | } 36 | 37 | .voting button { 38 | font-size: 2em; 39 | margin-right: .5em; 40 | } 41 | 42 | .voting .voting-row { 43 | display: flex; 44 | align-items: center; 45 | line-height: 1.5em; 46 | margin: .5em 0; 47 | } 48 | 49 | .voting .voting-count label { 50 | display: block; 51 | margin-bottom: 0; 52 | color: #333; 53 | font-weight: 400; 54 | } 55 | 56 | .voting .voting-number { 57 | font-size: 2em; 58 | } 59 | 60 | .histogram .baseline { 61 | height: 2px; 62 | background: #999999; 63 | width: 100%; 64 | } 65 | -------------------------------------------------------------------------------- /src/components/organisms/AccountSelector.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | Form, 5 | FormGroup, 6 | ControlLabel, 7 | FormControl 8 | } from 'react-bootstrap' 9 | 10 | import { 11 | getAccounts, 12 | setSelectedAccount 13 | } from './../../redux/actions/connection-actions' 14 | 15 | import './../../styles/organisms/AccountSelector.css' 16 | 17 | class AccountSelector extends Component { 18 | 19 | componentWillMount() { 20 | this.props.dispatch(getAccounts()) 21 | } 22 | 23 | setAddress(event) { 24 | const account = event.target.value 25 | this.props.dispatch(setSelectedAccount(account)) 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 | 32 | Registration Address 33 | 37 | { 38 | this.props.accounts.map((account, index) => { 39 | return 40 | }) 41 | } 42 | 43 | 44 |
45 | ) 46 | } 47 | 48 | } 49 | 50 | AccountSelector.propTypes = { 51 | dispatch: PropTypes.func, 52 | accounts: PropTypes.array, 53 | defaultAccount: PropTypes.string 54 | } 55 | 56 | export default AccountSelector 57 | -------------------------------------------------------------------------------- /src/redux/reducers/connection-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_NETWORK_STATUS_SUCCESS, 3 | GET_ACCOUNTS, 4 | SET_SELECTED_ACCOUNT 5 | } from './../actions/connection-actions' 6 | 7 | const initialState = { 8 | connected: false, 9 | currentBlock: 'SYNCING', 10 | currentBlockTime: 'SYNCING', 11 | secondsSinceLastBlock: 0, 12 | account: { 13 | items: [], 14 | selectedAccount: '' 15 | } 16 | } 17 | 18 | export default function connectionReducer(state = initialState, action) { 19 | 20 | switch (action.type) { 21 | 22 | case GET_ACCOUNTS: 23 | return Object.assign({}, state, { 24 | account: Object.assign({}, state.account, { 25 | items: action.accounts, 26 | selectedAccount: !state.account.selectedAccount ? 27 | action.accounts[0] : 28 | state.account.selectedAccount 29 | }) 30 | }) 31 | 32 | case SET_SELECTED_ACCOUNT: 33 | return Object.assign({}, state, { 34 | account: Object.assign({}, state.account, { 35 | selectedAccount: action.selectedAccount 36 | }) 37 | }) 38 | 39 | case FETCH_NETWORK_STATUS_SUCCESS: 40 | return Object.assign({}, state, { 41 | connected: action.response.connected, 42 | currentBlock: action.response.currentBlock, 43 | currentBlockTime: action.response.currentBlockTime, 44 | secondsSinceLastBlock: action.response.secondsSinceLastBlock 45 | }) 46 | 47 | default: 48 | return state 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trial 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | 4 | import { Router, Route, hashHistory, IndexRoute } from 'react-router' 5 | 6 | import { Provider } from 'react-redux' 7 | import { createStore, applyMiddleware } from 'redux' 8 | import appReducer from './redux/reducers' 9 | import createLogger from 'redux-logger' 10 | import thunk from 'redux-thunk' 11 | 12 | const logger = createLogger() 13 | const store = createStore( 14 | appReducer, 15 | applyMiddleware(thunk, logger) 16 | ) 17 | 18 | import Frame from './components/environments/Frame' 19 | import Home from './components/environments/Home' 20 | import About from './components/environments/About' 21 | import CliQuickstart from './components/environments/CliQuickstart' 22 | 23 | export const routes = [ 24 | { path: '/', name: 'Home', component: Home }, 25 | { path: '/about', name: 'About', component: About }, 26 | { path: '/cliquickstart', name: 'Cli QuickStart', component: CliQuickstart } 27 | ] 28 | 29 | import './index.css' 30 | import 'bootstrap/dist/css/bootstrap.min.css' 31 | import 'animate.css' 32 | 33 | import { 34 | getAccounts 35 | } from './redux/actions/connection-actions' 36 | 37 | store.dispatch(getAccounts()) 38 | 39 | render( 40 | 41 | 42 | 43 | 44 | { 45 | routes.slice(1).map((route, index) => { 46 | return ( 47 | 48 | ) 49 | }) 50 | } 51 | 52 | 53 | , 54 | document.getElementById('root') 55 | ) 56 | -------------------------------------------------------------------------------- /src/components/environments/Frame.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import './../../styles/environments/Frame.css' 3 | 4 | import { 5 | Navbar, 6 | Nav, 7 | NavItem 8 | } from 'react-bootstrap' 9 | 10 | import Alerts from './../ecosystems/Alerts' 11 | 12 | import logo from './../../images/logo.svg' 13 | import { routes } from './../../index' 14 | 15 | class Frame extends Component { 16 | 17 | handleSelect(selectedKey) { 18 | this.props.history.push(routes[selectedKey].path) 19 | } 20 | 21 | getActiveRouteIndex() { 22 | let hash = window.location.hash 23 | hash = hash.substring(hash.indexOf('#') + 1, hash.indexOf('?')) 24 | return routes.findIndex(route => { 25 | return hash === route.path 26 | }) 27 | } 28 | 29 | render() { 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | 37 | 38 | EtherSignal 39 | 40 | 41 | 54 | 55 | 56 |
57 | {this.props.children} 58 |
59 | 60 | 61 | 62 |
63 | ) 64 | 65 | } 66 | 67 | } 68 | 69 | Frame.propTypes = { 70 | children: PropTypes.node 71 | } 72 | 73 | export default Frame 74 | -------------------------------------------------------------------------------- /src/components/ecosystems/PositionSubmitter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import './../../styles/ecosystems/PositionSubmitter.css' 3 | 4 | import { 5 | Jumbotron, 6 | Button 7 | } from 'react-bootstrap' 8 | 9 | import PositionSubmitterModal from './../ecosystems/PositionSubmitterModal' 10 | 11 | import { 12 | showNewPositionModal 13 | } from './../../redux/actions/position-actions' 14 | 15 | import logo from './../../images/logo.svg' 16 | 17 | class PositionSubmitter extends Component { 18 | 19 | showModal() { 20 | this.props.dispatch(showNewPositionModal()) 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 |
29 |

30 | EtherSignal logo 31 | EtherSignal 32 |

33 |
34 |

Letting ether holders signal their positions.

35 |
36 |

40 |
41 | 42 | 49 | 50 |
51 | ) 52 | } 53 | 54 | } 55 | 56 | PositionSubmitter.propTypes = { 57 | dispatch: PropTypes.func, 58 | positions: PropTypes.object, 59 | connection: PropTypes.object 60 | } 61 | 62 | export default PositionSubmitter 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethersignal", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.3.7", 7 | "babel-core": "6.10.4", 8 | "babel-eslint": "6.1.2", 9 | "babel-loader": "6.2.4", 10 | "babel-plugin-syntax-trailing-function-commas": "6.8.0", 11 | "babel-plugin-transform-class-properties": "6.10.2", 12 | "babel-plugin-transform-object-rest-spread": "6.8.0", 13 | "babel-plugin-transform-react-constant-elements": "6.9.1", 14 | "babel-preset-es2015": "6.9.0", 15 | "babel-preset-es2016": "6.11.3", 16 | "babel-preset-react": "6.11.1", 17 | "chalk": "1.1.3", 18 | "cross-spawn": "4.0.0", 19 | "css-loader": "0.23.1", 20 | "eslint": "^3.3.0", 21 | "eslint-config-trails": "^1.0.7", 22 | "eslint-loader": "1.4.1", 23 | "eslint-plugin-import": "1.10.3", 24 | "eslint-plugin-react": "^5.2.2", 25 | "extract-text-webpack-plugin": "1.0.1", 26 | "file-loader": "0.9.0", 27 | "fs-extra": "^0.30.0", 28 | "html-webpack-plugin": "2.22.0", 29 | "json-loader": "0.5.4", 30 | "opn": "4.0.2", 31 | "postcss-loader": "0.9.1", 32 | "rimraf": "2.5.3", 33 | "style-loader": "0.13.1", 34 | "url-loader": "0.5.7", 35 | "webpack": "1.13.1", 36 | "webpack-dev-server": "1.14.1" 37 | }, 38 | "dependencies": { 39 | "animate.css": "^3.5.1", 40 | "bootstrap": "^3.3.7", 41 | "isomorphic-fetch": "^2.2.1", 42 | "lodash": "^4.14.1", 43 | "moment": "^2.14.1", 44 | "querystring": "^0.2.0", 45 | "react": "^15.2.1", 46 | "react-bootstrap": "^0.30.0", 47 | "react-d3": "^0.4.0", 48 | "react-d3-basic": "^1.6.11", 49 | "react-d3-shape": "^0.3.25", 50 | "react-dom": "^15.3.0", 51 | "react-redux": "^4.4.5", 52 | "react-router": "^2.6.0", 53 | "redux": "^3.5.2", 54 | "redux-logger": "^2.6.1", 55 | "redux-thunk": "^2.1.0", 56 | "web3": "^0.17.0-alpha" 57 | }, 58 | "scripts": { 59 | "start": "node ./scripts/start.js", 60 | "build": "node ./scripts/build.js" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/molecules/PositionDepositInput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | FormGroup, 5 | ControlLabel, 6 | InputGroup, 7 | FormControl, 8 | MenuItem, 9 | DropdownButton, 10 | HelpBlock 11 | } from 'react-bootstrap' 12 | 13 | import { 14 | setPositionDepositValue, 15 | setPositionDepositDenomination 16 | } from './../../redux/actions/position-actions' 17 | 18 | class PositionDepositInput extends Component { 19 | 20 | setValue(event) { 21 | const value = event.target.value 22 | this.props.dispatch(setPositionDepositValue(value)) 23 | } 24 | 25 | setDenomination(denomination) { 26 | this.props.dispatch(setPositionDepositDenomination(denomination)) 27 | } 28 | 29 | getValidationState() { 30 | return this.props.valueValidationError ? 'error' : '' 31 | } 32 | 33 | render() { 34 | return ( 35 | 38 | Value of Deposit 39 | 40 | 44 | 49 | Ether 50 | Finney 51 | Wei 52 | 53 | 54 | { this.props.valueValidationError } 55 | 56 | ) 57 | } 58 | 59 | } 60 | 61 | PositionDepositInput.propTypes = { 62 | dispatch: PropTypes.func, 63 | denomination: PropTypes.string, 64 | value: PropTypes.string, 65 | valueValidationError: PropTypes.string 66 | } 67 | 68 | export default PositionDepositInput 69 | -------------------------------------------------------------------------------- /src/components/organisms/PositionPagination.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | Form, 5 | Pagination, 6 | FormGroup, 7 | ControlLabel, 8 | FormControl 9 | } from 'react-bootstrap' 10 | 11 | import { 12 | setPositionPaginationItemsToDisplay, 13 | setPositionPaginationCurrentPage 14 | } from './../../redux/actions/position-actions' 15 | 16 | import './../../styles/organisms/PositionPagination.css' 17 | 18 | class PositionPagination extends Component { 19 | 20 | setItemsToDisplay(event) { 21 | const itemsToDisplay = event.target.value 22 | this.props.dispatch(setPositionPaginationItemsToDisplay(itemsToDisplay)) 23 | } 24 | 25 | setCurrentPage(currentPage) { 26 | this.props.dispatch(setPositionPaginationCurrentPage(currentPage)) 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | 33 | { 34 | Boolean(this.props.numberOfPages) && 35 | 41 | } 42 | 43 | 44 | Positions to Display 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | } 62 | 63 | PositionPagination.propTypes = { 64 | pagination: PropTypes.object, 65 | numberOfPages: PropTypes.number, 66 | dispatch: PropTypes.func 67 | } 68 | 69 | export default PositionPagination 70 | -------------------------------------------------------------------------------- /src/redux/actions/utils/getSignalPerBlock.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | 5 | module.exports = function getSignalPerBlock(input) { 6 | 7 | function removeUnclearVotes(input) { 8 | return input.filter(item => { 9 | return item.vote === '1' || item.vote === '0' 10 | }) 11 | } 12 | 13 | function groupByBlockNumber(input) { 14 | return _.groupBy(input, 'blockNumber') 15 | } 16 | 17 | function backfillHistory(input) { 18 | const keys = Object.keys(input) 19 | for (let blockNumber in input) { 20 | const index = keys.indexOf(blockNumber) 21 | if (index >= 1) { 22 | keys.slice(index - 1, index).forEach(lastBlock => { 23 | input[blockNumber] = dedupeTransactions(input[lastBlock].concat(input[blockNumber])) 24 | }) 25 | } 26 | } 27 | return input 28 | } 29 | 30 | function dedupeTransactions(input) { 31 | let output = [] 32 | const grouped = _.groupBy(input, 'from') 33 | for (let address in grouped) { 34 | output = [ 35 | ...output, 36 | ...grouped[address].slice(-1) 37 | ] 38 | } 39 | return output 40 | } 41 | 42 | function calculateSignal(input) { 43 | 44 | let output = [] 45 | 46 | for (let address in input) { 47 | input[address] = input[address].reduce((memo, current) => { 48 | if (current.vote === '1') memo.pro.push(current) 49 | if (current.vote === '0') memo.against.push(current) 50 | return memo 51 | }, {blockNumber: address, pro: [], against: []}) 52 | output.push(input[address]) 53 | } 54 | 55 | output = output.map(block => { 56 | block.pro = block.pro.reduce((memo, block) => { 57 | memo += Number(block.signal) / Math.pow(10, 18) 58 | return memo 59 | }, 0) 60 | block.against = block.against.reduce((memo, block) => { 61 | memo -= Number(block.signal) / Math.pow(10, 18) 62 | return memo 63 | }, 0) 64 | return block 65 | }) 66 | 67 | return output 68 | 69 | } 70 | 71 | return calculateSignal(backfillHistory(groupByBlockNumber(removeUnclearVotes(input)))) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/components/organisms/SignalChart.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | import _ from 'lodash' 4 | import { 5 | Chart, 6 | BarStack 7 | } from 'react-d3-shape' 8 | 9 | class SignalChart extends Component { 10 | 11 | render() { 12 | 13 | const chartSeries = [ 14 | { 15 | field: 'pro', 16 | name: 'Pro', 17 | style: { 18 | fill: '#5CB85C', 19 | strokeWidth: 5, 20 | strokeOpacity: 1, 21 | fillOpacity: 1 22 | } 23 | }, 24 | { 25 | field: 'against', 26 | name: 'Against', 27 | style: { 28 | fill: '#D9534F', 29 | strokeWidth: 5, 30 | strokeOpacity: 1, 31 | fillOpacity: 1 32 | } 33 | }, 34 | ] 35 | 36 | function getX(block) { 37 | return block.blockNumber 38 | } 39 | 40 | function getY(value) { 41 | return +value 42 | } 43 | 44 | function getYDomain(input) { 45 | const maxPro = _.get(_.maxBy(input, 'pro'), 'pro') || 0 46 | const maxAgainst = _.get(_.maxBy(input, 'against'), 'against') || 0 47 | 48 | const max = _.max([maxPro, Math.abs(maxAgainst)]) 49 | return [-max, max] 50 | } 51 | 52 | return ( 53 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | 74 | } 75 | 76 | SignalChart.propTypes = { 77 | data: PropTypes.array 78 | } 79 | 80 | export default SignalChart 81 | -------------------------------------------------------------------------------- /src/components/organisms/PositionDepositModal.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | Modal, 5 | Button 6 | } from 'react-bootstrap' 7 | 8 | import { 9 | addPositionDeposit, 10 | hidePositionDepositModal, 11 | setPositionDepositValidationError 12 | } from './../../redux/actions/position-actions' 13 | 14 | import PositionDepositInput from './../molecules/PositionDepositInput' 15 | 16 | class PositionDepositModal extends Component { 17 | 18 | makeDeposit() { 19 | 20 | if (!this.props.value) { 21 | this.props.dispatch(setPositionDepositValidationError('This value shouldn\'t be blank')) 22 | return 23 | } 24 | 25 | this.props.dispatch(addPositionDeposit( 26 | this.props.value, 27 | this.props.denomination, 28 | this.props.senderAddr, 29 | this.props.recipientAddr 30 | )) 31 | } 32 | 33 | hideModal() { 34 | this.props.dispatch(hidePositionDepositModal()) 35 | } 36 | 37 | render() { 38 | 39 | return ( 40 | 44 | 45 | 46 | How Much? 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | {' '} 61 | 64 | 65 | 66 | 67 | ) 68 | 69 | } 70 | 71 | } 72 | 73 | PositionDepositModal.propTypes = { 74 | showModal: PropTypes.bool, 75 | senderAddr: PropTypes.string, 76 | recipientAddr: PropTypes.string, 77 | value: PropTypes.string, 78 | valueValidationError: PropTypes.string, 79 | denomination: PropTypes.string, 80 | dispatch: PropTypes.func 81 | } 82 | 83 | export default PositionDepositModal 84 | -------------------------------------------------------------------------------- /src/components/ecosystems/PositionSubmitterModal.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | Modal, 5 | Button 6 | } from 'react-bootstrap' 7 | 8 | import { 9 | hideNewPositionModal, 10 | submitNewPosition, 11 | setNewPositionTitleValidationError 12 | } from './../../redux/actions/position-actions' 13 | 14 | import PositionSubmitterForm from './../organisms/PositionSubmitterForm' 15 | import AccountSelector from './../organisms/AccountSelector' 16 | 17 | class PositionSubmitterModal extends Component { 18 | 19 | hideModal() { 20 | this.props.dispatch(hideNewPositionModal()) 21 | } 22 | 23 | submitPosition() { 24 | 25 | if (!this.props.title || this.props.title.length < 2) { 26 | this.props.dispatch( 27 | setNewPositionTitleValidationError('The title should be longer than one character.') 28 | ) 29 | return 30 | } 31 | 32 | this.props.dispatch(submitNewPosition( 33 | this.props.title, 34 | this.props.description, 35 | this.props.connection.account.selectedAccount 36 | )) 37 | 38 | } 39 | 40 | render() { 41 | return ( 42 | 45 | 46 | 47 |

Create New Position

48 |
49 | 50 | 51 | 56 | 60 | 61 | 62 | 63 | {' '} 66 | 69 | 70 |
71 | ) 72 | } 73 | 74 | } 75 | 76 | PositionSubmitterModal.propTypes = { 77 | showModal: PropTypes.bool, 78 | title: PropTypes.string, 79 | description: PropTypes.string, 80 | titleValidationError: PropTypes.string, 81 | connection: PropTypes.object, 82 | dispatch: PropTypes.func 83 | } 84 | 85 | export default PositionSubmitterModal 86 | -------------------------------------------------------------------------------- /src/redux/actions/connection-actions.js: -------------------------------------------------------------------------------- 1 | /* global Web3, web3 */ 2 | 3 | if (typeof web3 !== 'undefined' && typeof Web3 !== 'undefined') { 4 | web3 = new Web3(web3.currentProvider) 5 | } 6 | else if (typeof Web3 !== 'undefined') { 7 | web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 8 | if (!web3.isConnected()) { 9 | const Web3 = require('web3') 10 | web3 = new Web3(new Web3.providers.HttpProvider('http://rpc.ethapi.org:8545')) 11 | } 12 | } 13 | 14 | import moment from 'moment' 15 | 16 | import { 17 | fetchPositions 18 | } from './../../redux/actions/position-actions' 19 | 20 | export const GET_ACCOUNTS = 'GET_ACCOUNTS' 21 | export const SET_SELECTED_ACCOUNT = 'SET_SELECTED_ACCOUNT' 22 | 23 | export function getAccounts() { 24 | return { 25 | type: GET_ACCOUNTS, 26 | accounts: web3.eth.accounts 27 | } 28 | } 29 | 30 | export function setSelectedAccount(selectedAccount) { 31 | return { 32 | type: SET_SELECTED_ACCOUNT, 33 | selectedAccount 34 | } 35 | } 36 | 37 | export const FETCH_NETWORK_STATUS_REQUEST = 'FETCH_NETWORK_STATUS_REQUEST' 38 | export const FETCH_NETWORK_STATUS_SUCCESS = 'FETCH_NETWORK_STATUS_SUCCESS' 39 | export const FETCH_NETWORK_STATUS_FAILURE = 'FETCH_NETWORK_STATUS_FAILURE' 40 | 41 | export function fetchNetworkStatusRequest() { 42 | return { 43 | type: FETCH_NETWORK_STATUS_REQUEST 44 | } 45 | } 46 | 47 | export function fetchNetworkStatusSuccess(response) { 48 | return { 49 | type: FETCH_NETWORK_STATUS_SUCCESS, 50 | response 51 | } 52 | } 53 | 54 | export function fetchNetworkStatusFailure(error) { 55 | return { 56 | type: FETCH_NETWORK_STATUS_FAILURE, 57 | error 58 | } 59 | } 60 | 61 | export function watchNetworkStatus() { 62 | 63 | function utcSecondsToString(timestamp) { 64 | return moment(timestamp * 1000).toDate().toString() 65 | } 66 | 67 | function getTimeSinceLastBlock(timestamp) { 68 | return Math.floor(moment().diff(moment(timestamp * 1000)) / 1000) 69 | } 70 | 71 | return dispatch => { 72 | const latestStatus = web3.eth.filter('latest') 73 | 74 | latestStatus.watch((err, blockHash) => { 75 | return new Promise((resolve, reject) => { 76 | web3.eth.getBlock(blockHash, false, function(err, block) { 77 | if (err) reject(err) 78 | resolve(block) 79 | }) 80 | }) 81 | .then(response => { 82 | dispatch(fetchPositions()) 83 | dispatch(fetchNetworkStatusSuccess({ 84 | connected: true, 85 | currentBlock: response.number, 86 | currentBlockTime: utcSecondsToString(response.timestamp), 87 | secondsSinceLastBlock: getTimeSinceLastBlock(response.timestamp) 88 | })) 89 | }) 90 | .catch(error => { 91 | dispatch(fetchNetworkStatusFailure(error)) 92 | }) 93 | }) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/components/organisms/NetworkStatus.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import './../../styles/organisms/NetworkStatus.css' 4 | 5 | import { 6 | watchNetworkStatus, 7 | } from './../../redux/actions/connection-actions' 8 | 9 | class NetworkStatus extends Component { 10 | 11 | constructor(props) { 12 | super(props) 13 | this.state = {} 14 | } 15 | 16 | componentWillMount() { 17 | this.setState({ 18 | secondsSinceLastBlock: 0, 19 | syncState: 'Pending' 20 | }) 21 | this.props.dispatch(watchNetworkStatus()) 22 | this.polling = setInterval(this.updateSyncStatus.bind(this), 1000) 23 | } 24 | 25 | componentWillUnmount() { 26 | clearInterval(this.polling) 27 | } 28 | 29 | componentWillReceiveProps(newProps) { 30 | if (newProps.connection.secondsSinceLastBlock !== this.state.secondsSinceLastBlock) { 31 | this.setState({ 32 | secondsSinceLastBlock: newProps.connection.secondsSinceLastBlock 33 | }) 34 | } 35 | } 36 | 37 | updateSyncStatus() { 38 | 39 | const connected = this.props.connection.connected 40 | function getSyncStatusText(timeSinceLastBlock) { 41 | 42 | if (!connected) return 'Pending' 43 | 44 | if (timeSinceLastBlock < 20) { 45 | return 'Good' 46 | } 47 | else if (timeSinceLastBlock > 20 && timeSinceLastBlock < 60) { 48 | return 'Warning' 49 | } 50 | else { 51 | return 'Bad' 52 | } 53 | } 54 | 55 | this.setState({ 56 | syncState: getSyncStatusText(this.state.secondsSinceLastBlock), 57 | secondsSinceLastBlock: this.state.secondsSinceLastBlock + 1 58 | }) 59 | 60 | } 61 | 62 | render() { 63 | 64 | return ( 65 | 66 | 67 | {' '} 68 | { 69 | this.props.connection.connected ? 70 | Connected : 71 | Disconnected 72 | }{' '} 73 | {this.state.syncState}{' '} 74 | ()
75 | 76 | {' '} 77 | {this.props.connection.currentBlock}
78 | 79 | {' '} 80 | {this.props.connection.currentBlockTime}
81 | 82 | { 83 | this.props.connection.connected && 84 |
85 | {' '} 86 | {this.state.secondsSinceLastBlock} seconds 87 |
88 | } 89 | 90 |
91 | ) 92 | } 93 | 94 | } 95 | 96 | NetworkStatus.propTypes = { 97 | dispatch: PropTypes.func, 98 | connection: PropTypes.object 99 | } 100 | 101 | export default NetworkStatus 102 | -------------------------------------------------------------------------------- /src/components/organisms/PositionSubmitterForm.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import _ from 'lodash' 3 | 4 | import { 5 | FormGroup, 6 | ControlLabel, 7 | FormControl, 8 | HelpBlock 9 | } from 'react-bootstrap' 10 | 11 | import { 12 | setNewPositionTitle, 13 | setNewPositionDescription 14 | } from './../../redux/actions/position-actions' 15 | 16 | class PositionSubmitterForm extends Component { 17 | 18 | constructor(props) { 19 | super(props) 20 | this.state = {} 21 | this.setDescription = _.debounce(this.setDescription, 300) 22 | this.setTitle = _.debounce(this.setTitle, 300) 23 | } 24 | 25 | componentWillMount() { 26 | this.setState({ 27 | title: this.props.title, 28 | description: this.props.description 29 | }) 30 | } 31 | 32 | onSetTitle(event) { 33 | const title = event.target.value 34 | this.setState({ 35 | title 36 | }, this.setTitle.bind(this, title)) 37 | } 38 | 39 | setTitle(title) { 40 | this.props.dispatch(setNewPositionTitle(title)) 41 | } 42 | 43 | onSetDescription(event) { 44 | const description = event.target.value 45 | this.setState({ 46 | description 47 | }, this.setDescription.bind(this, description)) 48 | } 49 | 50 | setDescription(description) { 51 | this.props.dispatch(setNewPositionDescription(description)) 52 | } 53 | 54 | getValidationState() { 55 | if (this.props.titleValidationError) return 'error' 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 | 64 | Title 65 | 70 | 71 | { 72 | this.props.titleValidationError && 73 | {this.props.titleValidationError} 74 | } 75 | 76 | 77 | 78 | Description 79 | 84 | 85 | 86 |
87 | ) 88 | } 89 | 90 | } 91 | 92 | PositionSubmitterForm.propTypes = { 93 | titleValidationError: PropTypes.string, 94 | title: PropTypes.string, 95 | description: PropTypes.string, 96 | dispatch: PropTypes.func 97 | } 98 | 99 | export default PositionSubmitterForm 100 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | // TODO: hide this behind a flag and eliminate dead code on eject. 7 | // This shouldn't be exposed to the user. 8 | var isInNodeModules = 'node_modules' === 9 | path.basename(path.resolve(path.join(__dirname, '..', '..'))); 10 | var relativePath = isInNodeModules ? '../../..' : '..'; 11 | var isInDebugMode = process.argv.some(arg => 12 | arg.indexOf('--debug-template') > -1 13 | ); 14 | if (isInDebugMode) { 15 | relativePath = '../template'; 16 | } 17 | var srcPath = path.resolve(__dirname, relativePath, 'src'); 18 | var nodeModulesPath = path.join(__dirname, '..', 'node_modules'); 19 | var indexHtmlPath = path.resolve(__dirname, relativePath, 'index.html'); 20 | var faviconPath = path.resolve(__dirname, relativePath, 'favicon.ico'); 21 | var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build'); 22 | 23 | module.exports = { 24 | devtool: 'eval', 25 | entry: [ 26 | require.resolve('webpack-dev-server/client') + '?http://localhost:8080', 27 | require.resolve('webpack/hot/dev-server'), 28 | path.join(srcPath, 'index') 29 | ], 30 | output: { 31 | // Next line is not used in dev but WebpackDevServer crashes without it: 32 | path: buildPath, 33 | pathinfo: true, 34 | filename: 'bundle.js', 35 | publicPath: '/' 36 | }, 37 | devServer: { 38 | headers: { "Access-Control-Allow-Origin": "*" } 39 | }, 40 | resolve: { 41 | extensions: ['', '.js'], 42 | }, 43 | resolveLoader: { 44 | root: nodeModulesPath, 45 | moduleTemplates: ['*-loader'] 46 | }, 47 | module: { 48 | preLoaders: [ 49 | { 50 | test: /\.js$/, 51 | loader: 'eslint', 52 | include: srcPath, 53 | } 54 | ], 55 | loaders: [ 56 | { 57 | test: /\.js$/, 58 | include: srcPath, 59 | loader: 'babel', 60 | query: require('./babel.dev') 61 | }, 62 | { 63 | test: /\.css$/, 64 | include: [ 65 | srcPath, 66 | nodeModulesPath 67 | ], 68 | loader: 'style!css!postcss' 69 | }, 70 | { 71 | test: /\.json$/, 72 | loader: 'json' 73 | }, 74 | { 75 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/, 76 | loader: 'file', 77 | }, 78 | { 79 | test: /\.(mp4|webm)$/, 80 | loader: 'url?limit=10000' 81 | } 82 | ] 83 | }, 84 | eslint: { 85 | configFile: path.join(__dirname, 'eslint.js'), 86 | useEslintrc: false 87 | }, 88 | postcss: function() { 89 | return [autoprefixer]; 90 | }, 91 | plugins: [ 92 | new HtmlWebpackPlugin({ 93 | inject: true, 94 | template: indexHtmlPath, 95 | favicon: faviconPath, 96 | }), 97 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), 98 | // Note: only CSS is currently hot reloaded 99 | new webpack.HotModuleReplacementPlugin() 100 | ] 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/organisms/PositionFilter.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import './../../styles/organisms/PositionFilter.css' 4 | 5 | import { 6 | Form, 7 | FormGroup, 8 | ControlLabel, 9 | FormControl, 10 | InputGroup, 11 | DropdownButton, 12 | MenuItem 13 | } from 'react-bootstrap' 14 | 15 | import { 16 | setPositionOrderBy, 17 | setPositionMinimumValueFilter, 18 | setPositionMiniumValueDenomination, 19 | } from './../../redux/actions/position-actions' 20 | 21 | class PositionFilter extends Component { 22 | 23 | setSort(event) { 24 | const value = event.target.value.split('.') 25 | const orderBy = value[0] 26 | const direction = value[1] 27 | this.props.dispatch(setPositionOrderBy(orderBy, direction)) 28 | } 29 | 30 | setDenomination(denomination) { 31 | this.props.dispatch(setPositionMiniumValueDenomination(denomination)) 32 | } 33 | 34 | setMinimumValue(event) { 35 | const minimumValue = event.target.value 36 | this.props.dispatch(setPositionMinimumValueFilter(minimumValue)) 37 | } 38 | 39 | onSubmitForm(event) { 40 | event.preventDefault() 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 | 47 | 48 | Sort by 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Minimum Value 64 | 65 | 70 | 75 | Ether 76 | Finney 77 | Wei 78 | 79 | 80 | 81 | 82 |
83 | ) 84 | } 85 | 86 | } 87 | 88 | PositionFilter.propTypes = { 89 | sort: PropTypes.object, 90 | filter: PropTypes.object, 91 | dispatch: PropTypes.func 92 | } 93 | 94 | export default PositionFilter 95 | -------------------------------------------------------------------------------- /cli/readmev2.txt: -------------------------------------------------------------------------------- 1 | EtherSignal CLI 2 | 3 | Quick Start. 4 | 5 | 1) Launch a geth node if it is not already running. 6 | geth 7 | 8 | 2) Attach via the geth command line client 9 | geth attach 10 | 11 | 3) Load the ethersignal script. 12 | > loadScript("ethersignal2.js") 13 | true 14 | 15 | Now you can either signal on a position, tally the current signal levels 16 | for a position, list the registered positions, register a position, or 17 | adjust the balance of a position you have registered to change 18 | its visibility: 19 | 20 | === To list the registered positions run the following: 21 | > ListPositions() 22 | [Positions: cut & paste the CalcSignal(); portion to see current signal levels] 23 | 24 | Position CalcSignal("0x953521cfe06b48d65b64ae864abb4c808312885e", 1297010); 25 | registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78 26 | eth deposit: 0 27 | Title: will this work 28 | Text: will this contract factory work 29 | 30 | Position CalcSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", 1297014); 31 | registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78 32 | eth deposit: 0 33 | Title: title 34 | Text: text 35 | Positions filtered for being under the minDeposit of 0: 0 36 | true 37 | 38 | === If you would like to filter based on the position desposit, pass a 39 | parameter to ListPositions) as follows: 40 | > ListPositions(1) 41 | [Positions: cut & paste the CalcSignal(); portion to see current signal levels] 42 | Positions filtered for being under the minDeposit of 1: 2 43 | true 44 | 45 | === As you can see above, in order to get the current signal levels for a position 46 | you can simply cut and paste the CalcSignal(); portion of the output from 47 | ListPositions(): 48 | > CalcSignal("0x953521cfe06b48d65b64ae864abb4c808312885e", 1297010); 49 | { 50 | against: 0, 51 | pro: 167.12471268213704 52 | } 53 | 54 | === In order to register a position you can use the following contract method 55 | > positionregistry.registerPosition("title", "text", {from: web3.eth.accounts[0], gas: 300000}); 56 | 57 | === If you would like to optionally submit a deposit into your position 58 | in order to distinguish it from others you can do the following (note 59 | that your deposit will be returned when you withdraw the position). 60 | Also you can deposit and withdraw multiple times to adjust its 61 | balance adaptively: 62 | > web3.eth.sendTransaction({from: web3.eth.accounts[0], to:"0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", value: web3.toWei(0.1, "ether")}) 63 | 64 | === You may withdraw ether from your registered position as follows (the unit here is ether not wei) 65 | > WithdrawFromPosition("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", 0.1); 66 | 67 | === In order to vote on a position, you will need to use the positions 68 | signal address. Take the following signal as an example: 69 | Position CalcSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f"); 70 | registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78 71 | eth deposit: 0 72 | Title: title 73 | Text: text 74 | 75 | The signal address is what is within CalcSignal(); so above it is 76 | "0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f". To vote simply run the 77 | following command, where true means to vote for the signal, and false 78 | would mean to vote against the signal: 79 | > SetSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", true); 80 | 81 | Enjoy. 82 | -------------------------------------------------------------------------------- /src/components/molecules/PositionListItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import './../../styles/molecules/PositionListItem.css' 3 | 4 | import { 5 | Button, 6 | Glyphicon 7 | } from 'react-bootstrap' 8 | 9 | import { 10 | voteOnPosition, 11 | displayPositionDepositModal 12 | } from './../../redux/actions/position-actions' 13 | 14 | import SignalChart from './../organisms/SignalChart' 15 | 16 | class PositionListItem extends Component { 17 | 18 | vote(proposalId, vote) { 19 | // TODO: If a selected account is not available, alert the user. 20 | this.props.dispatch(voteOnPosition(proposalId, vote)) 21 | } 22 | 23 | addPositionDeposit() { 24 | this.props.dispatch(displayPositionDepositModal( 25 | this.props.account, 26 | this.props.position.sigAddr 27 | )) 28 | } 29 | 30 | render() { 31 | 32 | return ( 33 |
34 | 35 |
36 | 37 |

{this.props.position.title}

38 |

{this.props.position.desc}

39 | 40 |
41 | 42 |
{this.props.position.regAddr}
43 |
44 | 45 |
46 | 47 |
48 | 49 |
{this.props.position.deposit}
50 |
51 | 52 | {' '} 53 | 54 | { 55 | this.props.position.isMine && 56 | 62 | } 63 | 64 |
65 | 66 |
67 | 68 | 69 | 70 |
71 | 72 |
73 | 78 |
79 | 80 | {this.props.position.pro} eth 81 |
82 |
83 | 84 |
85 | 90 |
91 | 92 | {this.props.position.against} eth 93 |
94 |
95 | 96 |
97 | 98 |
99 | ) 100 | } 101 | 102 | } 103 | 104 | PositionListItem.propTypes = { 105 | dispatch: PropTypes.func, 106 | position: PropTypes.object, 107 | account: PropTypes.string 108 | } 109 | 110 | export default PositionListItem 111 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | // TODO: hide this behind a flag and eliminate dead code on eject. 8 | // This shouldn't be exposed to the user. 9 | var isInNodeModules = 'node_modules' === 10 | path.basename(path.resolve(path.join(__dirname, '..', '..'))); 11 | var relativePath = isInNodeModules ? '../../..' : '..'; 12 | if (process.argv[2] === '--debug-template') { 13 | relativePath = '../template'; 14 | } 15 | var srcPath = path.resolve(__dirname, relativePath, 'src'); 16 | var nodeModulesPath = path.join(__dirname, '..', 'node_modules'); 17 | var indexHtmlPath = path.resolve(__dirname, relativePath, 'index.html'); 18 | var faviconPath = path.resolve(__dirname, relativePath, 'favicon.ico'); 19 | var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build'); 20 | 21 | module.exports = { 22 | bail: true, 23 | devtool: 'source-map', 24 | entry: path.join(srcPath, 'index'), 25 | output: { 26 | path: buildPath, 27 | filename: '[name].[chunkhash].js', 28 | chunkFilename: '[name].[chunkhash].chunk.js', 29 | // TODO: this wouldn't work for e.g. GH Pages. 30 | // Good news: we can infer it from package.json :-) 31 | publicPath: '/' 32 | }, 33 | resolve: { 34 | extensions: ['', '.js'], 35 | }, 36 | resolveLoader: { 37 | root: nodeModulesPath, 38 | moduleTemplates: ['*-loader'] 39 | }, 40 | module: { 41 | preLoaders: [ 42 | { 43 | test: /\.js$/, 44 | loader: 'eslint', 45 | include: srcPath 46 | } 47 | ], 48 | loaders: [ 49 | { 50 | test: /\.js$/, 51 | include: srcPath, 52 | loader: 'babel', 53 | query: require('./babel.prod') 54 | }, 55 | { 56 | test: /\.css$/, 57 | include: srcPath, 58 | // Disable autoprefixer in css-loader itself: 59 | // https://github.com/webpack/css-loader/issues/281 60 | // We already have it thanks to postcss. 61 | loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss') 62 | }, 63 | { 64 | test: /\.json$/, 65 | loader: 'json' 66 | }, 67 | { 68 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/, 69 | loader: 'file', 70 | }, 71 | { 72 | test: /\.(mp4|webm)$/, 73 | loader: 'url?limit=10000' 74 | } 75 | ] 76 | }, 77 | eslint: { 78 | // TODO: consider separate config for production, 79 | // e.g. to enable no-console and no-debugger only in prod. 80 | configFile: path.join(__dirname, 'eslint.js'), 81 | useEslintrc: false 82 | }, 83 | postcss: function() { 84 | return [autoprefixer]; 85 | }, 86 | plugins: [ 87 | new HtmlWebpackPlugin({ 88 | inject: true, 89 | template: indexHtmlPath, 90 | favicon: faviconPath, 91 | minify: { 92 | removeComments: true, 93 | collapseWhitespace: true, 94 | removeRedundantAttributes: true, 95 | useShortDoctype: true, 96 | removeEmptyAttributes: true, 97 | removeStyleLinkTypeAttributes: true, 98 | keepClosingSlash: true, 99 | minifyJS: true, 100 | minifyCSS: true, 101 | minifyURLs: true 102 | } 103 | }), 104 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), 105 | new webpack.optimize.OccurrenceOrderPlugin(), 106 | new webpack.optimize.DedupePlugin(), 107 | new webpack.optimize.UglifyJsPlugin({ 108 | compressor: { 109 | screw_ie8: true, 110 | warnings: false 111 | }, 112 | mangle: { 113 | screw_ie8: true 114 | }, 115 | output: { 116 | comments: false, 117 | screw_ie8: true 118 | } 119 | }), 120 | new ExtractTextPlugin('[name].[contenthash].css') 121 | ] 122 | }; 123 | -------------------------------------------------------------------------------- /cli/ethersignal2.js: -------------------------------------------------------------------------------- 1 | var ethersignalContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"pro","type":"bool"}],"name":"setSignal","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"endSignal","outputs":[],"type":"function"},{"inputs":[{"name":"rAddr","type":"address"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"pro","type":"bool"},{"indexed":false,"name":"addr","type":"address"}],"name":"LogSignal","type":"event"},{"anonymous":false,"inputs":[],"name":"EndSignal","type":"event"}]); 2 | 3 | var positionregistryContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"title","type":"string"},{"name":"text","type":"string"}],"name":"registerPosition","outputs":[],"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"regAddr","type":"address"},{"indexed":true,"name":"sigAddr","type":"address"},{"indexed":false,"name":"title","type":"string"},{"indexed":false,"name":"text","type":"string"}],"name":"LogPosition","type":"event"}]); 4 | var positionregistry = positionregistryContract.at('0x17351fb5e243ebf9c4480734c010a875853f8d9e') 5 | 6 | /* 7 | function WithdrawPosition(sigAddr) { 8 | var ethersignal = ethersignalContract.at(sigAddr); 9 | 10 | ethersignal.endSignal(); 11 | 12 | return true; 13 | } 14 | */ 15 | 16 | function WithdrawFromPosition(sigAddr, amount) { 17 | var ethersignal = ethersignalContract.at(sigAddr); 18 | var gas = ethersignal.withdraw.estimateGas(web3.toWei(amount)) * 2; 19 | 20 | ethersignal.withdraw(web3.toWei(amount), {from: web3.eth.accounts[0], gas: gas}); 21 | 22 | return true; 23 | } 24 | 25 | function SetSignal(sigAddr, pro) { 26 | var ethersignal = ethersignalContract.at(sigAddr); 27 | var gas = ethersignal.setSignal.estimateGas(pro); 28 | 29 | ethersignal.setSignal(pro, {from: web3.eth.accounts[0], gas: gas}); 30 | 31 | return true; 32 | } 33 | 34 | function ListPositions(minDeposit) { 35 | var posMap = {}; 36 | var minDeposit = typeof minDeposit !== 'undefined' ? minDeposit : 0; 37 | 38 | positionregistry.LogPosition({}, {fromBlock: 1200000}, function(error, result){ 39 | if (!error) 40 | { 41 | posMap[result.args.sigAddr] = [result.args.title, result.args.text, result.args.regAddr, result.blockNumber]; 42 | } 43 | }) 44 | 45 | console.log("[Positions: cut & paste the CalcSignal(); portion to see current signal levels]"); 46 | 47 | var numFiltered = 0; 48 | Object.keys(posMap).map(function(k) { 49 | var deposit = web3.fromWei(web3.eth.getBalance(k)); 50 | 51 | if (deposit >= minDeposit) 52 | { 53 | console.log("\nPosition CalcSignal(\"" + k + "\"," + posMap[k][3] + ");"); 54 | console.log(" registered by " + posMap[k][2]); 55 | console.log(" eth deposit: " + deposit); 56 | console.log("Title: " + posMap[k][0]); 57 | console.log("Text: " + posMap[k][1]); 58 | } else { 59 | numFiltered++; 60 | } 61 | }); 62 | 63 | console.log("Positions filtered for being under the minDeposit of " + minDeposit + ": " + numFiltered); 64 | 65 | return true; 66 | } 67 | 68 | function CalcSignal(posAddr, startBlock) { 69 | var proMap = {}; 70 | var antiMap = {}; 71 | 72 | var ethersignal = ethersignalContract.at(posAddr); 73 | 74 | ethersignal.LogSignal({}, {fromBlock: startBlock}, function(error, result){ 75 | if (!error) 76 | { 77 | if (result.args.pro) { 78 | proMap[result.args.addr] = 1; 79 | antiMap[result.args.addr] = 0; 80 | } else { 81 | proMap[result.args.addr] = 0; 82 | antiMap[result.args.addr] = 1; 83 | } 84 | } 85 | }) 86 | 87 | var totalPro = 0; 88 | var totalAgainst = 0; 89 | 90 | // call getBalance just once per address 91 | Object.keys(proMap).map(function(a) { 92 | var bal = web3.fromWei(web3.eth.getBalance(a)); 93 | proMap[a] = proMap[a] * bal; 94 | antiMap[a] = antiMap[a] * bal; 95 | }); 96 | 97 | // sum the pro and anti account values 98 | Object.keys(proMap).map(function(a) { totalPro += parseFloat(proMap[a]); }); 99 | Object.keys(antiMap).map(function(a) { totalAgainst += parseFloat(antiMap[a]); }); 100 | 101 | return {pro: totalPro, against: totalAgainst} 102 | } 103 | -------------------------------------------------------------------------------- /src/components/ecosystems/Positions.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import NetworkStatus from './../organisms/NetworkStatus' 4 | import PositionFilter from './../organisms/PositionFilter' 5 | import PositionList from './../organisms/PositionList' 6 | import PositionPagination from './../organisms/PositionPagination' 7 | import PositionDepositModal from './../organisms/PositionDepositModal' 8 | 9 | import _ from 'lodash' 10 | 11 | import { 12 | Panel 13 | } from 'react-bootstrap' 14 | 15 | import { 16 | fetchPositions 17 | } from './../../redux/actions/position-actions' 18 | 19 | class Positions extends Component { 20 | 21 | componentWillMount() { 22 | this.props.dispatch(fetchPositions()) 23 | } 24 | 25 | processPositions(positions) { 26 | 27 | const itemsToDisplay = this.props.positions.pagination.itemsToDisplay 28 | const orderBy = this.props.positions.sort.orderBy 29 | const direction = this.props.positions.sort.direction 30 | const minimumValue = this.props.positions.filter.minimumValue 31 | const denomination = this.props.positions.filter.denomination 32 | 33 | function filterPositions(positions) { 34 | 35 | /* 36 | * deposit values are returned by the ABI as finney. 37 | */ 38 | 39 | if (minimumValue === '') return positions 40 | 41 | let multiplier 42 | 43 | switch (denomination) { 44 | 45 | case 'Wei': 46 | // If input is in wei, multiply the deposit value by: 1e^15 47 | multiplier = Math.pow(10, 15) 48 | break 49 | 50 | case 'Finney': 51 | // If input is in finney, good to go. 52 | multiplier = 1 53 | break 54 | 55 | case 'Ether': 56 | // If input is in ether, divide the input by 1000 to convert to finney 57 | multiplier = (1 / 1000) 58 | break 59 | 60 | default: 61 | multiplier = 1 62 | break 63 | } 64 | 65 | return positions.filter(position => { 66 | return position.deposit * multiplier >= minimumValue 67 | }) 68 | 69 | } 70 | 71 | function sortPositions(positions) { 72 | return _.orderBy(positions, [orderBy], [direction]) 73 | } 74 | 75 | function chunkPositions(positions) { 76 | return _.chunk(positions, itemsToDisplay) 77 | } 78 | 79 | return chunkPositions(sortPositions(filterPositions(positions))) 80 | 81 | } 82 | 83 | 84 | render() { 85 | 86 | const positions = this.processPositions(this.props.positions.items) 87 | let index = this.props.positions.pagination.currentPage - 1 88 | if (positions.length < index) { 89 | index = positions.length - 1 90 | } 91 | const positionsToRender = positions[index] || [] 92 | const pagination = Object.assign({}, this.props.positions.pagination, { currentPage: index + 1}) 93 | 94 | return ( 95 | , 100 | 105 | ]} 106 | footer={ 107 | 111 | }> 112 | 120 | 125 | 126 | ) 127 | } 128 | 129 | } 130 | 131 | Positions.propTypes = { 132 | dispatch: PropTypes.func, 133 | connection: PropTypes.object, 134 | positions: PropTypes.object 135 | } 136 | 137 | export default Positions 138 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var webpack = require('webpack'); 6 | var WebpackDevServer = require('webpack-dev-server'); 7 | var config = require('../config/webpack.config.dev'); 8 | var execSync = require('child_process').execSync; 9 | var opn = require('opn'); 10 | 11 | // TODO: hide this behind a flag and eliminate dead code on eject. 12 | // This shouldn't be exposed to the user. 13 | var handleCompile; 14 | var isSmokeTest = process.argv.some(arg => 15 | arg.indexOf('--smoke-test') > -1 16 | ); 17 | if (isSmokeTest) { 18 | handleCompile = function (err, stats) { 19 | if (err || stats.hasErrors() || stats.hasWarnings()) { 20 | process.exit(1); 21 | } else { 22 | process.exit(0); 23 | } 24 | }; 25 | } 26 | 27 | var friendlySyntaxErrorLabel = 'Syntax error:'; 28 | 29 | function isLikelyASyntaxError(message) { 30 | return message.indexOf(friendlySyntaxErrorLabel) !== -1; 31 | } 32 | 33 | // This is a little hacky. 34 | // It would be easier if webpack provided a rich error object. 35 | 36 | function formatMessage(message) { 37 | return message 38 | // Make some common errors shorter: 39 | .replace( 40 | // Babel syntax error 41 | 'Module build failed: SyntaxError:', 42 | friendlySyntaxErrorLabel 43 | ) 44 | .replace( 45 | // Webpack file not found error 46 | /Module not found: Error: Cannot resolve 'file' or 'directory'/, 47 | 'Module not found:' 48 | ) 49 | // Internal stacks are generally useless so we strip them 50 | .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y 51 | // Webpack loader names obscure CSS filenames 52 | .replace('./~/css-loader!./~/postcss-loader!', ''); 53 | } 54 | 55 | function clearConsole() { 56 | process.stdout.write('\x1B[2J\x1B[0f'); 57 | } 58 | 59 | var compiler = webpack(config, handleCompile); 60 | compiler.plugin('invalid', function () { 61 | clearConsole(); 62 | console.log('Compiling...'); 63 | }); 64 | compiler.plugin('done', function (stats) { 65 | clearConsole(); 66 | var hasErrors = stats.hasErrors(); 67 | var hasWarnings = stats.hasWarnings(); 68 | if (!hasErrors && !hasWarnings) { 69 | console.log(chalk.green('Compiled successfully!')); 70 | console.log(); 71 | console.log('The app is running at http://localhost:8080/'); 72 | console.log(); 73 | return; 74 | } 75 | 76 | var json = stats.toJson(); 77 | var formattedErrors = json.errors.map(message => 78 | 'Error in ' + formatMessage(message) 79 | ); 80 | var formattedWarnings = json.warnings.map(message => 81 | 'Warning in ' + formatMessage(message) 82 | ); 83 | 84 | if (hasErrors) { 85 | console.log(chalk.red('Failed to compile.')); 86 | console.log(); 87 | if (formattedErrors.some(isLikelyASyntaxError)) { 88 | // If there are any syntax errors, show just them. 89 | // This prevents a confusing ESLint parsing error 90 | // preceding a much more useful Babel syntax error. 91 | formattedErrors = formattedErrors.filter(isLikelyASyntaxError); 92 | } 93 | formattedErrors.forEach(message => { 94 | console.log(message); 95 | console.log(); 96 | }); 97 | // If errors exist, ignore warnings. 98 | return; 99 | } 100 | 101 | if (hasWarnings) { 102 | console.log(chalk.yellow('Compiled with warnings.')); 103 | console.log(); 104 | formattedWarnings.forEach(message => { 105 | console.log(message); 106 | console.log(); 107 | }); 108 | 109 | console.log('You may use special comments to disable some warnings.'); 110 | console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.'); 111 | console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); 112 | } 113 | }); 114 | 115 | function openBrowser() { 116 | if (process.platform === 'darwin') { 117 | try { 118 | // Try our best to reuse existing tab 119 | // on OS X Google Chrome with AppleScript 120 | execSync('ps cax | grep "Google Chrome"'); 121 | execSync( 122 | 'osascript ' + 123 | path.resolve(__dirname, './openChrome.applescript') + 124 | ' http://localhost:8080/' 125 | ); 126 | return; 127 | } catch (err) { 128 | // Ignore errors. 129 | } 130 | } 131 | // Fallback to opn 132 | // (It will always open new tab) 133 | opn('http://localhost:8080/'); 134 | } 135 | 136 | new WebpackDevServer(compiler, { 137 | historyApiFallback: true, 138 | hot: true, // Note: only CSS is currently hot reloaded 139 | publicPath: config.output.publicPath, 140 | quiet: true 141 | }).listen(8080, 'localhost', function (err, result) { 142 | if (err) { 143 | return console.log(err); 144 | } 145 | 146 | clearConsole(); 147 | console.log(chalk.cyan('Starting the development server...')); 148 | console.log(); 149 | // openBrowser(); 150 | }); 151 | -------------------------------------------------------------------------------- /src/components/environments/CliQuickstart.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class CliQuickstart extends Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

EtherSignal CLI

9 |

Quick Start.

10 | 11 |
12 |

13 | 1. Launch a geth node if it is not already running. 14 |

geth
15 |

16 | 17 |

18 | 2. Attach via the geth command line client 19 |

geth attach
20 |

21 | 22 |

23 | 3. Load the ethersignal script. 24 |

 25 |             > loadScript("ethersignal2.js"){'\n'}
 26 |             true
 27 |           
28 |

29 | 30 |
31 | 32 |

33 | Now you can either signal on a position, tally the current signal levels 34 | for a position, list the registered positions, or register a position: 35 |

36 | 37 |

To list the registered positions run the following: 38 |

 39 |             > ListPositions() {'\n'}
 40 |             [Positions: cut & paste the CalcSignal(); portion to see current signal levels]{'\n'}
 41 |             {'\n'}
 42 |             {'\n'}
 43 |             Position CalcSignal("0x953521cfe06b48d65b64ae864abb4c808312885e", 1290010);{'\n'}
 44 |             registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78{'\n'}
 45 |             eth deposit: 0{'\n'}
 46 |             Title: will this work{'\n'}
 47 |             Text: will this contract factory work{'\n'}
 48 |             {'\n'}
 49 |             Position CalcSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", 1290020);{'\n'}
 50 |             registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78{'\n'}
 51 |             eth deposit: 0{'\n'}
 52 |             Title: title{'\n'}
 53 |             Text: text{'\n'}
 54 |             Positions filtered for being under the minDeposit of 0: 0{'\n'}
 55 |             true{'\n'}
 56 |           
57 |

58 | 59 |
60 |

61 | If you would like to filter based on the position desposit, pass a 62 | parameter to ListPositions() as follows: 63 |

 64 |             > ListPositions(1){'\n'}
 65 |             [Positions: cut & paste the CalcSignal(); portion to see current signal levels]{'\n'}
 66 |             Positions filtered for being under the minDeposit of 1: 2{'\n'}
 67 |             true{'\n'}
 68 |           
69 |

70 | 71 |
72 |

73 | As you can see above, in order to get the current signal levels for a position 74 | you can simply cut and paste the CalcSignal(); portion of the output from: 75 |

 76 |             ListPositions():{'\n'}
 77 |             > CalcSignal("0x953521cfe06b48d65b64ae864abb4c808312885e", 1290010);{'\n'}
 78 |             {'{'}{'\n'}
 79 |             {'  '}against: 0,{'\n'}
 80 |             {'  '}pro: 167.12471268213704{'\n'}
 81 |             {'}'}{'\n'}
 82 |           
83 |

84 |
85 | 86 |

87 | In order to register a position you can use the following contract method: 88 |

 89 |             > positionregistry.registerPosition("title", "text",
 90 |             {'{'}from: web3.eth.accounts[0], gas: 300000{'}'});
 91 |           
92 |

93 | 94 |
95 |

96 | If you would like to optionally submit a deposit into your position 97 | in order to distinguish it from others you can do the following (note 98 | your deposit will be returned when you withdraw the position): 99 |

100 |             > web3.eth.sendTransaction({'{'}from: web3.eth.accounts[0],
101 |             to:"0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", value: web3.toWei(0.1, "ether"){'}'})
102 |           
103 |

104 |
105 | 106 |

You may withdraw you position and reclaim your deposit as follows: 107 |

> WithdrawPosition("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f");
108 |

109 | 110 |
111 |

In order to vote on a position, you will need to use the positions 112 | signal address. Take the following signal as an example: 113 |

114 |             Position CalcSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", 1290020);{'\n'}
115 |             registered by 0x8c2741b9bebd3c27feb7bb3356f7b04652977b78{'\n'}
116 |             eth deposit: 0{'\n'}
117 |             Title: title{'\n'}
118 |             Text: text{'\n'}
119 |           
120 |

121 | 122 |
123 |

The signal address is what is within CalcSignal(); so above it is 124 | "0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f". To vote simply run the 125 | following command, where true means to vote for the signal, and false 126 | would mean to vote against the signal: 127 |

> SetSignal("0xcdda0a8fe9a7a844c9d8611b2cadfe36b4bb438f", true);
128 |

129 | 130 |

Enjoy.

131 | 132 |
133 | ) 134 | } 135 | 136 | } 137 | 138 | CliQuickstart.propTypes = {} 139 | 140 | export default CliQuickstart 141 | -------------------------------------------------------------------------------- /src/redux/reducers/position-reducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import { 4 | FETCH_POSITIONS_REQUEST, 5 | FETCH_POSITIONS_SUCCESS, 6 | FETCH_POSITIONS_FAILURE, 7 | SHOW_NEW_POSITION_MODAL, 8 | HIDE_NEW_POSITION_MODAL, 9 | SET_NEW_POSITION_TITLE, 10 | SET_NEW_POSITION_DESCRIPTION, 11 | SET_NEW_POSITION_TITLE_VALIDATION_ERROR, 12 | SUBMIT_NEW_POSITION_SUCCESS, 13 | SUBMIT_NEW_POSITION_FAILURE, 14 | SET_POSITION_ORDER_BY, 15 | SET_POSITION_MINIMUM_VALUE_FILTER, 16 | SET_POSITION_MINIMUM_VALUE_DENOMINATION, 17 | SET_POSITION_PAGINATION_ITEMS_TO_DISPLAY, 18 | SET_POSITION_PAGINATION_CURRENT_PAGE, 19 | SET_POSITION_PAGINATION_NUMBER_OF_PAGES, 20 | DISPLAY_POSITION_DEPOSIT_MODAL, 21 | HIDE_POSITION_DEPOSIT_MODAL, 22 | SET_POSITION_DEPOSIT_VALUE, 23 | SET_POSITION_DEPOSIT_VALIDATION_ERROR, 24 | SET_POSITION_DEPOSIT_DENOMINATION 25 | } from './../actions/position-actions' 26 | 27 | const initialState = { 28 | showModal: false, 29 | fetching: false, 30 | error: '', 31 | items: [], 32 | sort: { 33 | orderBy: 'absoluteSignal', 34 | direction: 'desc' 35 | }, 36 | filter: { 37 | minimumValue: '', 38 | denomination: 'Ether' 39 | }, 40 | pagination: { 41 | itemsToDisplay: 5, 42 | currentPage: 1 43 | }, 44 | newPosition: { 45 | title: '', 46 | description: '', 47 | titleValidationError: '' 48 | }, 49 | depositModal: { 50 | showModal: false, 51 | senderAddr: '', 52 | recipientAddr: '', 53 | value: '', 54 | valueValidationError: '', 55 | denomination: 'Finney' 56 | } 57 | } 58 | 59 | export default function positionReducer(state = initialState, action) { 60 | 61 | switch (action.type) { 62 | 63 | case DISPLAY_POSITION_DEPOSIT_MODAL: 64 | return Object.assign({}, state, { 65 | depositModal: Object.assign({}, state.depositModal, { 66 | showModal: true, 67 | senderAddr: action.senderAddr, 68 | recipientAddr: action.recipientAddr 69 | }) 70 | }) 71 | 72 | case HIDE_POSITION_DEPOSIT_MODAL: 73 | return Object.assign({}, state, { 74 | depositModal: Object.assign({}, initialState.depositModal) 75 | }) 76 | 77 | case SET_POSITION_DEPOSIT_VALUE: 78 | return Object.assign({}, state, { 79 | depositModal: Object.assign({}, state.depositModal, { 80 | value: action.value, 81 | valueValidationError: '' 82 | }) 83 | }) 84 | 85 | case SET_POSITION_DEPOSIT_VALIDATION_ERROR: 86 | return Object.assign({}, state, { 87 | depositModal: Object.assign({}, state.depositModal, { 88 | valueValidationError: action.error 89 | }) 90 | }) 91 | 92 | case SET_POSITION_DEPOSIT_DENOMINATION: 93 | return Object.assign({}, state, { 94 | depositModal: Object.assign({}, state.depositModal, { 95 | denomination: action.denomination 96 | }) 97 | }) 98 | 99 | case FETCH_POSITIONS_REQUEST: 100 | return Object.assign({}, state, { 101 | fetching: true, 102 | error: '' 103 | }) 104 | 105 | case FETCH_POSITIONS_SUCCESS: 106 | return Object.assign({}, state, { 107 | fetching: false, 108 | error: '', 109 | items: [ 110 | ...action.response 111 | ] 112 | }) 113 | 114 | case FETCH_POSITIONS_FAILURE: 115 | return Object.assign({}, state, { 116 | fetching: false, 117 | error: '' 118 | }) 119 | 120 | case SHOW_NEW_POSITION_MODAL: 121 | return Object.assign({}, state, { 122 | showModal: true 123 | }) 124 | 125 | case SET_NEW_POSITION_TITLE: 126 | return Object.assign({}, state, { 127 | newPosition: Object.assign({}, state.newPosition, { 128 | title: action.title, 129 | titleValidationError: '' 130 | }) 131 | }) 132 | 133 | case SET_NEW_POSITION_DESCRIPTION: 134 | return Object.assign({}, state, { 135 | newPosition: Object.assign({}, state.newPosition, { 136 | description: action.description 137 | }) 138 | }) 139 | 140 | case HIDE_NEW_POSITION_MODAL: 141 | case SUBMIT_NEW_POSITION_FAILURE: 142 | case SUBMIT_NEW_POSITION_SUCCESS: 143 | return Object.assign({}, state, { 144 | showModal: false, 145 | newPosition: Object.assign({}, initialState.newPosition) 146 | }) 147 | 148 | case SET_NEW_POSITION_TITLE_VALIDATION_ERROR: 149 | return Object.assign({}, state, { 150 | newPosition: Object.assign({}, state.newPosition, { 151 | titleValidationError: action.error 152 | }) 153 | }) 154 | 155 | case SET_POSITION_ORDER_BY: 156 | return Object.assign({}, state, { 157 | sort: Object.assign({}, state.sort, { 158 | orderBy: action.orderBy, 159 | direction: action.direction 160 | }) 161 | }) 162 | 163 | case SET_POSITION_MINIMUM_VALUE_FILTER: 164 | return Object.assign({}, state, { 165 | filter: Object.assign({}, state.filter, { 166 | minimumValue: action.minimumValue 167 | }) 168 | }) 169 | 170 | case SET_POSITION_MINIMUM_VALUE_DENOMINATION: 171 | return Object.assign({}, state, { 172 | filter: Object.assign({}, state.filter, { 173 | denomination: action.denomination 174 | }) 175 | }) 176 | 177 | case SET_POSITION_PAGINATION_ITEMS_TO_DISPLAY: 178 | return Object.assign({}, state, { 179 | pagination: Object.assign({}, state.pagination, { 180 | itemsToDisplay: _.toNumber(action.itemsToDisplay) 181 | }) 182 | }) 183 | 184 | case SET_POSITION_PAGINATION_CURRENT_PAGE: 185 | return Object.assign({}, state, { 186 | pagination: Object.assign({}, state.pagination, { 187 | currentPage: action.currentPage 188 | }) 189 | }) 190 | 191 | case SET_POSITION_PAGINATION_NUMBER_OF_PAGES: 192 | return Object.assign({}, state, { 193 | pagination: Object.assign({}, state.pagination, { 194 | numberOfPages: action.numberOfPages 195 | }) 196 | }) 197 | 198 | default: 199 | return state 200 | 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /src/redux/actions/position-actions.js: -------------------------------------------------------------------------------- 1 | import querystring from 'querystring' 2 | import fetch from 'isomorphic-fetch' 3 | import getSignalPerBlock from './utils/getSignalPerBlock' 4 | import _ from 'lodash' 5 | 6 | /* 7 | * connection to local blockchain node. 8 | */ 9 | 10 | /* global Web3, web3 */ 11 | 12 | import etherSignalAbi from './abi/etherSignalAbi' 13 | import positionRegistryAbi from './abi/positionRegistryAbi' 14 | 15 | if (typeof web3 !== 'undefined' && typeof Web3 !== 'undefined') { 16 | web3 = new Web3(web3.currentProvider) 17 | } 18 | else if (typeof Web3 !== 'undefined') { 19 | web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 20 | if (!web3.isConnected()) { 21 | const Web3 = require('web3') 22 | web3 = new Web3(new Web3.providers.HttpProvider('http://rpc.ethapi.org:8545')) 23 | } 24 | } 25 | 26 | import { 27 | addTimedAlert 28 | } from './alert-actions' 29 | 30 | /* 31 | * What contracts are we interested in? 32 | */ 33 | 34 | const etherSignalContract = web3.eth.contract(etherSignalAbi) 35 | const positionRegistryContract = web3.eth.contract(positionRegistryAbi) 36 | 37 | const address = '0x9e75993a7a9b9f92a1978bcc15c30cbcb967bc81' 38 | const positionRegistry = positionRegistryContract.at(address) 39 | 40 | /* 41 | * Begin Redux actions 42 | */ 43 | 44 | export const FETCH_POSITIONS_REQUEST = 'FETCH_POSITIONS_REQUEST' 45 | export const FETCH_POSITIONS_SUCCESS = 'FETCH_POSITIONS_SUCCESS' 46 | export const FETCH_POSITIONS_FAILURE = 'FETCH_POSITIONS_FAILURE' 47 | 48 | export function fetchPositionsRequest() { 49 | return { 50 | type: FETCH_POSITIONS_REQUEST 51 | } 52 | } 53 | 54 | export function fetchPositionsSuccess(response) { 55 | return { 56 | type: FETCH_POSITIONS_SUCCESS, 57 | response 58 | } 59 | } 60 | 61 | export function fetchPositionsFailure(error) { 62 | return { 63 | type: FETCH_POSITIONS_FAILURE, 64 | error 65 | } 66 | } 67 | 68 | /* 69 | * @name getPositions 70 | * @param {number} fromBlock 71 | * @param {number} endBlock 72 | * @description 73 | * Makes a call to the positionRegistry ABI and returns positions contained in 74 | * blocks ranging from ${fromBlock} to ${endBlock} 75 | */ 76 | 77 | function getPositions(fromBlock, endBlock) { 78 | return new Promise((resolve, reject) => { 79 | positionRegistry.LogPosition({}, {fromBlock, endBlock}) 80 | .get((err, positions) => { 81 | if (err) reject(err) 82 | resolve(positions) 83 | }) 84 | }) 85 | } 86 | 87 | /* 88 | * @name getPositionDeposit 89 | * @param {object} position 90 | * A position returned by #getPositions 91 | * @description 92 | * Calculates the current deposit (or amplitude) held at the signal address for 93 | * a given position 94 | */ 95 | 96 | function getPositionDeposit(position) { 97 | return new Promise((resolve, reject) => { 98 | web3.eth.getBlock(position.blockNumber, (err, block) => { 99 | web3.eth.getBalance(position.args.sigAddr, (err, balance) => { 100 | const deposit = Number(web3.fromWei(balance, 'finney')) 101 | resolve(Object.assign({}, position, {block, deposit})) 102 | }) 103 | }) 104 | }) 105 | } 106 | 107 | /* 108 | * @name getPositionVoteMaps 109 | * @description 110 | * Accepts a position object and collects signal transactions starting from 111 | * the positions block number. 112 | * @returns 113 | * Position object with proMap and againstMap properties, which describe which 114 | * accounts voted for and against the position. 115 | */ 116 | 117 | function getPositionVoteMaps(position) { 118 | 119 | const address = position.args.sigAddr 120 | const etherSignal = etherSignalContract.at(address) 121 | 122 | return new Promise((resolve, reject) => { 123 | 124 | etherSignal.LogSignal({}, {fromBlock: position.blockNumber}) 125 | .get((error, signals) => { 126 | 127 | if (error) { 128 | reject(error) 129 | } 130 | 131 | const proMap = {} 132 | const againstMap = {} 133 | 134 | signals.forEach(signal => { 135 | if (signal.args.pro) { 136 | proMap[signal.args.addr] = 1 137 | againstMap[signal.args.addr] = 0 138 | } 139 | else { 140 | proMap[signal.args.addr] = 0 141 | againstMap[signal.args.addr] = 1 142 | } 143 | }) 144 | 145 | resolve(Object.assign({}, position, {proMap, againstMap})) 146 | 147 | }) 148 | }) 149 | } 150 | 151 | /* 152 | * @name calculateCurrentSignal 153 | * @param {object} position 154 | * A position object with properties proMap and againstMap, which are provided by 155 | * #getPositionVoteMaps 156 | * @description 157 | * Iterates over addresses in proMap and againstMap and calculates their current 158 | * account balance. 159 | */ 160 | 161 | function calculateCurrentSignal(position) { 162 | 163 | position.totalPro = 0 164 | position.totalAgainst = 0 165 | position.isMine = false 166 | position.iHaveSignalled = false 167 | position.myVote 168 | 169 | return Promise.all( 170 | _.map(position.proMap, (key, address) => { 171 | return new Promise((resolve, reject) => { 172 | web3.eth.getBalance(address, (err, balance) => { 173 | 174 | balance = web3.fromWei(balance) 175 | 176 | position.proMap[address] = position.proMap[address] * balance 177 | position.againstMap[address] = position.againstMap[address] * balance 178 | 179 | position.totalPro += parseFloat(position.proMap[address]) 180 | position.totalAgainst += parseFloat(position.againstMap[address]) 181 | 182 | web3.eth.accounts.find(account => { 183 | if (address === account) { 184 | position.iHaveSignalled = true 185 | if (position.proMap[address]) { 186 | position.myVote = 'pro' 187 | } 188 | else if (position.againstMap[address]) { 189 | position.myVote = 'against' 190 | } 191 | } 192 | }) 193 | 194 | }) 195 | 196 | resolve() 197 | 198 | }) 199 | 200 | }) 201 | ) 202 | .then(() => { 203 | 204 | for (const index in web3.eth.accounts) { 205 | if (web3.eth.accounts[index] === position.args.regAddr) { 206 | position.isMine = true 207 | } 208 | } 209 | 210 | return position 211 | 212 | }) 213 | 214 | } 215 | 216 | /* 217 | * @name #formatPosition 218 | * @param {object} position 219 | * @description 220 | * Formats a position object after all the operations responsible for adding 221 | * data have been completed. 222 | */ 223 | 224 | function formatPosition(position) { 225 | return { 226 | title: position.args.title, 227 | desc: position.args.text, 228 | regAddr: position.args.regAddr, 229 | pro: Math.round(position.totalPro), 230 | against: Math.round(position.totalAgainst), 231 | absoluteSignal: position.totalPro + Math.abs(position.totalAgainst), 232 | sigAddr: position.args.sigAddr, 233 | deposit: position.deposit, 234 | creationDate: position.block.timestamp, 235 | iHaveSignalled: position.iHaveSignalled, 236 | isMine: position.isMine, 237 | myVote: position.myVote, 238 | history: position.history 239 | } 240 | } 241 | 242 | export function fetchPositions(fromBlock = 1200000, endBlock) { 243 | return dispatch => { 244 | dispatch(fetchPositionsRequest()) 245 | getPositions(fromBlock, endBlock) 246 | .then(positions => { 247 | return Promise.all(positions.map(position => getPositionDeposit(position))) 248 | }) 249 | .then(positions => { 250 | return Promise.all(positions.map(position => getPositionVoteMaps(position))) 251 | }) 252 | .then(positions => { 253 | return Promise.all(positions.map(position => calculateCurrentSignal(position))) 254 | }) 255 | .then(positions => { 256 | return Promise.all(positions.map(position => fetchHistoricalSignal(position))) 257 | }) 258 | .then(positions => { 259 | return positions.map(position => formatPosition(position)) 260 | }) 261 | .then(positions => { 262 | dispatch(fetchPositionsSuccess(positions)) 263 | }) 264 | .catch(error => { 265 | dispatch(fetchPositionsFailure(error)) 266 | }) 267 | } 268 | } 269 | 270 | export const VOTE_ON_POSITION_REQUEST = 'VOTE_ON_POSITION_REQUEST' 271 | export const VOTE_ON_POSITION_SUCCESS = 'VOTE_ON_POSITION_SUCCESS' 272 | export const VOTE_ON_POSITION_FAILURE = 'VOTE_ON_POSITION_FAILURE' 273 | 274 | export function voteOnPositionRequest() { 275 | return { 276 | type: VOTE_ON_POSITION_REQUEST 277 | } 278 | } 279 | 280 | export function voteOnPositionSuccess(response) { 281 | return { 282 | type: VOTE_ON_POSITION_SUCCESS, 283 | response 284 | } 285 | } 286 | 287 | export function voteOnPositionFailure(error) { 288 | return { 289 | type: VOTE_ON_POSITION_FAILURE, 290 | error 291 | } 292 | } 293 | 294 | // Casts a a vote for against a given position for all accounts that are active. 295 | export function voteOnPosition(positionSignalAddress, vote) { 296 | 297 | // If vote is true, it is a vote in favor of the given position. 298 | // Else, it is a vote against the position. 299 | const etherSignal = etherSignalContract.at(positionSignalAddress) 300 | 301 | return dispatch => { 302 | Promise.all( 303 | web3.eth.accounts.map(account => { 304 | return new Promise((resolve, reject) => { 305 | try { 306 | resolve(etherSignal.setSignal(vote, {from: account})) 307 | } 308 | catch (err) { 309 | reject(err) 310 | } 311 | }) 312 | }) 313 | ) 314 | .then(response => { 315 | dispatch(addTimedAlert('Your vote was submitted!', 'success')) 316 | dispatch(voteOnPositionSuccess(response)) 317 | }) 318 | .catch(error => { 319 | dispatch(addTimedAlert(error.message, 'danger')) 320 | dispatch(voteOnPositionFailure(error)) 321 | }) 322 | } 323 | 324 | } 325 | 326 | export const SHOW_NEW_POSITION_MODAL = 'SHOW_NEW_POSITION_MODAL' 327 | export const HIDE_NEW_POSITION_MODAL = 'HIDE_NEW_POSITION_MODAL' 328 | 329 | export function showNewPositionModal() { 330 | return { 331 | type: SHOW_NEW_POSITION_MODAL 332 | } 333 | } 334 | 335 | export function hideNewPositionModal() { 336 | return { 337 | type: HIDE_NEW_POSITION_MODAL 338 | } 339 | } 340 | 341 | export const SET_NEW_POSITION_TITLE = 'SET_NEW_POSITION_TITLE' 342 | export const SET_NEW_POSITION_DESCRIPTION = 'SET_NEW_POSITION_DESCRIPTION' 343 | export const SET_NEW_POSITION_TITLE_VALIDATION_ERROR = 'SET_NEW_POSITION_TITLE_VALIDATION_ERROR' 344 | 345 | export function setNewPositionTitle(title) { 346 | return { 347 | type: SET_NEW_POSITION_TITLE, 348 | title 349 | } 350 | } 351 | 352 | export function setNewPositionDescription(description) { 353 | return { 354 | type: SET_NEW_POSITION_DESCRIPTION, 355 | description 356 | } 357 | } 358 | 359 | export function setNewPositionTitleValidationError(error) { 360 | return { 361 | type: SET_NEW_POSITION_TITLE_VALIDATION_ERROR, 362 | error 363 | } 364 | } 365 | 366 | export const SUBMIT_NEW_POSITION_REQUEST = 'SUBMIT_NEW_POSITION_REQUEST' 367 | export const SUBMIT_NEW_POSITION_SUCCESS = 'SUBMIT_NEW_POSITION_SUCCESS' 368 | export const SUBMIT_NEW_POSITION_FAILURE = 'SUBMIT_NEW_POSITION_FAILURE' 369 | 370 | export function submitNewPositionRequest() { 371 | return { 372 | type: SUBMIT_NEW_POSITION_REQUEST 373 | } 374 | } 375 | 376 | export function submitNewPositionSuccess(response) { 377 | return { 378 | type: SUBMIT_NEW_POSITION_SUCCESS, 379 | response 380 | } 381 | } 382 | 383 | export function submitNewPositionFailure(error) { 384 | return { 385 | type: SUBMIT_NEW_POSITION_FAILURE, 386 | error 387 | } 388 | } 389 | 390 | export function submitNewPosition(title, description, account) { 391 | 392 | // Todo: there should be an account selector 393 | const sender = account 394 | const data = positionRegistry.registerPosition.getData(title, description) 395 | 396 | return dispatch => { 397 | dispatch(submitNewPositionRequest()) 398 | web3.eth.estimateGas({from: sender, to: address, data: data}, (err, gas) => { 399 | try { 400 | positionRegistry.registerPosition.sendTransaction( 401 | title, 402 | description, 403 | { 404 | from: sender, 405 | to: address, 406 | gas: gas 407 | }, 408 | (err, result) => { 409 | if (err) throw err 410 | dispatch(addTimedAlert('The position was submitted!', 'success')) 411 | dispatch(submitNewPositionSuccess(result)) 412 | } 413 | ) 414 | } 415 | 416 | catch (error) { 417 | dispatch(addTimedAlert(error.message, 'danger')) 418 | dispatch(submitNewPositionFailure(error)) 419 | } 420 | 421 | }) 422 | 423 | } 424 | 425 | } 426 | 427 | export const SET_POSITION_ORDER_BY = 'SET_POSITION_ORDER_BY' 428 | export const SET_POSITION_MINIMUM_VALUE_FILTER = 'SET_POSITION_MINIMUM_VALUE_FILTER' 429 | export const SET_POSITION_MINIMUM_VALUE_DENOMINATION = 'SET_POSITION_MINIMUM_VALUE_DENOMINATION' 430 | export const SET_POSITION_PAGINATION_ITEMS_TO_DISPLAY = 'SET_POSITION_PAGINATION_ITEMS_TO_DISPLAY' 431 | export const SET_POSITION_PAGINATION_CURRENT_PAGE = 'SET_POSITION_PAGINATION_CURRENT_PAGE' 432 | export const SET_POSITION_PAGINATION_NUMBER_OF_PAGES = 'SET_POSITION_PAGINATION_NUMBER_OF_PAGES' 433 | 434 | export function setPositionOrderBy(orderBy, direction) { 435 | return { 436 | type: SET_POSITION_ORDER_BY, 437 | orderBy, 438 | direction 439 | } 440 | } 441 | 442 | export function setPositionMinimumValueFilter(minimumValue) { 443 | return { 444 | type: SET_POSITION_MINIMUM_VALUE_FILTER, 445 | minimumValue 446 | } 447 | } 448 | 449 | export function setPositionMiniumValueDenomination(denomination) { 450 | return { 451 | type: SET_POSITION_MINIMUM_VALUE_DENOMINATION, 452 | denomination 453 | } 454 | } 455 | 456 | export function setPositionPaginationItemsToDisplay(itemsToDisplay) { 457 | return { 458 | type: SET_POSITION_PAGINATION_ITEMS_TO_DISPLAY, 459 | itemsToDisplay 460 | } 461 | } 462 | 463 | export function setPositionPaginationCurrentPage(currentPage) { 464 | return { 465 | type: SET_POSITION_PAGINATION_CURRENT_PAGE, 466 | currentPage 467 | } 468 | } 469 | 470 | export function setPositionPaginationNumberOfPages(numberOfPages) { 471 | return { 472 | type: SET_POSITION_PAGINATION_NUMBER_OF_PAGES, 473 | numberOfPages 474 | } 475 | } 476 | 477 | export const DISPLAY_POSITION_DEPOSIT_MODAL = 'DISPLAY_POSITION_DEPOSIT_MODAL' 478 | export const HIDE_POSITION_DEPOSIT_MODAL = 'HIDE_POSITION_DEPOSIT_MODAL' 479 | export const SET_POSITION_DEPOSIT_VALUE = 'SET_POSITION_DEPOSIT_VALUE' 480 | export const SET_POSITION_DEPOSIT_DENOMINATION = 'SET_POSITION_DEPOSIT_DENOMINATION' 481 | export const SET_POSITION_DEPOSIT_VALIDATION_ERROR = 'SET_POSITION_DEPOSIT_VALIDATION_ERROR' 482 | export const ADD_POSITION_DEPOSIT_REQUEST = 'ADD_POSITION_DEPOSIT_REQUEST' 483 | export const ADD_POSITION_DEPOSIT_SUCCESS = 'ADD_POSITION_DEPOSIT_SUCCESS' 484 | export const ADD_POSITION_DEPOSIT_FAILURE = 'ADD_POSITION_DEPOSIT_FAILURE' 485 | 486 | export function displayPositionDepositModal(senderAddr, recipientAddr) { 487 | return { 488 | type: DISPLAY_POSITION_DEPOSIT_MODAL, 489 | senderAddr, 490 | recipientAddr 491 | } 492 | } 493 | 494 | export function hidePositionDepositModal() { 495 | return { 496 | type: HIDE_POSITION_DEPOSIT_MODAL 497 | } 498 | } 499 | 500 | export function setPositionDepositValue(value) { 501 | return { 502 | type: SET_POSITION_DEPOSIT_VALUE, 503 | value 504 | } 505 | } 506 | 507 | export function setPositionDepositDenomination(denomination) { 508 | return { 509 | type: SET_POSITION_DEPOSIT_DENOMINATION, 510 | denomination 511 | } 512 | } 513 | 514 | export function setPositionDepositValidationError(error) { 515 | return { 516 | type: SET_POSITION_DEPOSIT_VALIDATION_ERROR, 517 | error 518 | } 519 | } 520 | 521 | export function addPositionDepositRequest() { 522 | return { 523 | type: ADD_POSITION_DEPOSIT_REQUEST 524 | } 525 | } 526 | 527 | export function addPositionDepositSuccess(response) { 528 | return { 529 | type: ADD_POSITION_DEPOSIT_REQUEST, 530 | response 531 | } 532 | } 533 | 534 | export function addPositionDepositFailure(error) { 535 | return { 536 | type: ADD_POSITION_DEPOSIT_FAILURE, 537 | error 538 | } 539 | } 540 | 541 | function denominationToWeiConverter(value, denomination) { 542 | 543 | switch (denomination) { 544 | 545 | case 'Ether': 546 | return value * Math.pow(10, 18) 547 | case 'Finney': 548 | return value * Math.pow(10, 15) 549 | case 'Wei': 550 | default: 551 | return value 552 | 553 | } 554 | 555 | } 556 | 557 | export function addPositionDeposit(value, denomination, senderAddr, recipientAddr) { 558 | 559 | return dispatch => { 560 | dispatch(addPositionDepositRequest()) 561 | return new Promise((resolve, reject) => { 562 | web3.eth.sendTransaction({ 563 | value: denominationToWeiConverter(value, denomination), 564 | from: senderAddr, 565 | to: recipientAddr 566 | }, (err, result) => { 567 | if (err) reject(err) 568 | resolve(result) 569 | }) 570 | }) 571 | .then(response => { 572 | dispatch(addTimedAlert('The deposit was submitted successfully', 'success')) 573 | dispatch(hidePositionDepositModal()) 574 | dispatch(addPositionDepositSuccess(response)) 575 | }) 576 | .catch(error => { 577 | dispatch(addTimedAlert('The transaction was not confirmed', 'danger')) 578 | dispatch(hidePositionDepositModal()) 579 | dispatch(addPositionDepositFailure(error)) 580 | }) 581 | } 582 | 583 | } 584 | 585 | export const GET_POSITION_SIGNAL_HISTORY_REQUEST = 'GET_POSITION_SIGNAL_HISTORY_REQUEST' 586 | export const GET_POSITION_SIGNAL_HISTORY_SUCCESS = 'GET_POSITION_SIGNAL_HISTORY_SUCCESS' 587 | export const GET_POSITION_SIGNAL_HISTORY_FAILURE = 'GET_POSITION_SIGNAL_HISTORY_FAILURE' 588 | 589 | export function getPositionSignalHistoryRequest() { 590 | return { 591 | type: GET_POSITION_SIGNAL_HISTORY_REQUEST 592 | } 593 | } 594 | 595 | export function getPositionSignalHistorySuccess(response) { 596 | return { 597 | type: GET_POSITION_SIGNAL_HISTORY_SUCCESS, 598 | response 599 | } 600 | } 601 | 602 | export function getPositionSignalHistoryFailure(error) { 603 | return { 604 | type: GET_POSITION_SIGNAL_HISTORY_FAILURE, 605 | error 606 | } 607 | } 608 | 609 | export function fetchHistoricalSignal(position, opts) { 610 | 611 | const contractAddress = position.args.sigAddr 612 | 613 | const URL = `https://ethersignal-api.herokuapp.com/transaction/${contractAddress}` 614 | 615 | if (!opts) { 616 | opts = {} 617 | } 618 | 619 | const params = { 620 | startblock: opts.startblock || 0, 621 | endblock: opts.endblock || 99999999, 622 | sort: opts.sort || 'asc' 623 | } 624 | 625 | const query = querystring.stringify(params) 626 | 627 | return fetch(`${URL}?${query}`, { 628 | method: 'GET' 629 | }) 630 | .then(response => response.json()) 631 | .then(response => { 632 | 633 | if (response.message === 'NOTOK') { 634 | throw 'There was an error with the testnet API.' 635 | } 636 | 637 | return Promise.all( 638 | 639 | response.result.map(transaction => { 640 | 641 | return new Promise((resolve, reject) => { 642 | 643 | web3.eth.getBalance(transaction.from, transaction.blockNumber, (err, balance) => { 644 | 645 | if (!balance) { 646 | console.log('No balance was returned for this transaction') // eslint-disable-line no-console 647 | console.log(transaction) // eslint-disable-line no-console 648 | } 649 | 650 | resolve({ 651 | from: transaction.from, 652 | blockNumber: transaction.blockNumber, 653 | signal: balance, 654 | vote: transaction.input.slice(-1) 655 | }) 656 | 657 | }) 658 | }) 659 | }) 660 | ) 661 | }) 662 | .then(response => { 663 | return getSignalPerBlock(response) 664 | }) 665 | .then(history => { 666 | return Object.assign({}, position, {history}) 667 | }) 668 | .catch(error => { 669 | // Recover from an error by passing an empty history 670 | return Object.assign({}, position, {history: []}) 671 | }) 672 | 673 | } 674 | --------------------------------------------------------------------------------