├── .nvmrc ├── app ├── actions │ ├── contracts │ │ ├── index.js │ │ └── SimpleStorage.js │ ├── index.js │ ├── ipfs.js │ └── accounts.js ├── init │ ├── storage.js │ ├── web3.js │ ├── contracts.js │ └── index.js ├── reducers │ ├── index.js │ ├── ipfs.js │ ├── accounts.js │ └── contracts │ │ └── SimpleStorage.js ├── index.html ├── index.js ├── style │ └── app.css ├── store.js └── components │ └── App │ └── index.js ├── .eslintignore ├── scripts ├── start_services.sh └── build_docker.sh ├── docker-compose.yml ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── .babelrc ├── .gitignore ├── truffle.js ├── contracts ├── SimpleStorage.sol └── Migrations.sol ├── .travis.yml ├── test └── simplestorage.js ├── spec └── actions │ └── accounts.spec.js ├── README.md ├── webpack.config.js ├── .eslintrc └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 7.6.0 2 | -------------------------------------------------------------------------------- /app/actions/contracts/index.js: -------------------------------------------------------------------------------- 1 | export * from './SimpleStorage' 2 | -------------------------------------------------------------------------------- /app/init/storage.js: -------------------------------------------------------------------------------- 1 | import ipfs from 'ipfs-api' 2 | 3 | export { 4 | ipfs 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/** 2 | build/** 3 | node_modules/** 4 | contracts/** 5 | submodules/** 6 | -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './accounts' 2 | export * from './contracts' 3 | export * from './ipfs' 4 | -------------------------------------------------------------------------------- /scripts/start_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$(pwd -P)/$(dirname $0)" 4 | 5 | echo "Starting Docker..." 6 | docker-compose up -d 7 | echo "Docker initialised..." 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | testrpc: 5 | image: freddielindsey/ethereumjs-testrpc 6 | container_name: testrpc 7 | ports: 8 | - 8545:8545 9 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('../contracts/Migrations.sol') 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations) 5 | } 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const SimpleStorage = artifacts.require('../contracts/SimpleStorage.sol') 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(SimpleStorage); 5 | }; 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "latest", 4 | "stage-0", 5 | "react" 6 | ], 7 | "env": { 8 | "development": { 9 | "presets": ["react-hmre"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Caches and packages 2 | .truffle-solidity-loader 3 | .cache 4 | node_modules 5 | .node-xml* 6 | chains.json 7 | 8 | # Logs 9 | yarn-error.log 10 | npm-debug.log 11 | 12 | # Build 13 | build 14 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | // Allows us to use ES6 in our migrations and tests. 2 | require('babel-register') 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: 'localhost', 8 | port: 8545, 9 | network_id: '*' // Match any network id 10 | } 11 | }, 12 | mocha: { 13 | useColors: true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import accounts from './accounts' 4 | import ipfs from './ipfs' 5 | 6 | // Contracts 7 | import SimpleStorage from './contracts/SimpleStorage' 8 | 9 | const reducers = combineReducers({ 10 | accounts, 11 | ipfs, 12 | SimpleStorage 13 | }); 14 | 15 | export default reducers; 16 | -------------------------------------------------------------------------------- /contracts/SimpleStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.8; 2 | 3 | contract SimpleStorage { 4 | uint public storedData; 5 | 6 | function SimpleStorage() { 7 | storedData = 10; 8 | } 9 | 10 | function set(uint x) { 11 | storedData = x; 12 | } 13 | 14 | function get() constant returns (uint retVal) { 15 | return storedData; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: node_js 4 | 5 | cache: yarn 6 | 7 | notification: 8 | email: 9 | success: never 10 | failure: always 11 | 12 | install: 13 | - yarn install 14 | 15 | before_script: 16 | - yarn run testrpc > ./testrpc.log & 17 | 18 | script: 19 | - yarn run build 20 | - yarn run test-contracts 21 | - yarn run test-web 22 | -------------------------------------------------------------------------------- /scripts/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_ROOT_TAG=freddielindsey/ethereumjs-testrpc 4 | DOCKER_TAG=$DOCKER_ROOT_TAG:$(date +%Y-%m-%d) 5 | SCRIPT_DIR="$(pwd -P)/$(dirname $0)" 6 | 7 | docker build -t $DOCKER_TAG "$SCRIPT_DIR/../submodules/testrpc" 8 | docker tag $DOCKER_TAG $DOCKER_ROOT_TAG:latest 9 | docker push $DOCKER_TAG 10 | docker push $DOCKER_ROOT_TAG:latest 11 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple Storage Demo 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/simplestorage.js: -------------------------------------------------------------------------------- 1 | const SimpleStorage = artifacts.require('./SimpleStorage.sol') 2 | 3 | contract('SimpleStorage', (accounts) => { 4 | 5 | it("should put 10 as the default value", () => { 6 | return SimpleStorage.deployed() 7 | .then((instance) => { 8 | return instance.get.call() 9 | }) 10 | .then((storedData) => { 11 | assert.equal(storedData.valueOf(), 10, "10 wasn't stored by default") 12 | }) 13 | }) 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // Import the page's CSS. Webpack will know what to do with it. 2 | import './style/app.css' 3 | 4 | import React from 'react' 5 | import { Provider } from 'react-redux' 6 | import { render } from 'react-dom' 7 | 8 | import './init' 9 | 10 | import store from './store' 11 | 12 | import App from './components/App/index.js' 13 | 14 | render( 15 | 16 | 17 | , 18 | document.getElementById('app') 19 | ) 20 | -------------------------------------------------------------------------------- /app/init/web3.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | let web3; 4 | if (typeof window !== 'undefined' && 5 | typeof window.web3 !== 'undefined') { 6 | // Use Mist/MetaMask's provider 7 | web3 = new Web3(window.web3.currentProvider) 8 | } else { 9 | // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) 10 | web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 11 | } 12 | 13 | export { 14 | web3 15 | } 16 | -------------------------------------------------------------------------------- /app/init/contracts.js: -------------------------------------------------------------------------------- 1 | import contract from 'truffle-contract' 2 | 3 | import SimpleStorage from '../../contracts/SimpleStorage.sol' 4 | 5 | export const contractArtifacts = { 6 | SimpleStorage 7 | } 8 | 9 | export const initialise = (contractArtifacts, web3) => { 10 | let contracts = {}; 11 | for (const k in contractArtifacts) { 12 | const contract_ = contract(contractArtifacts[k]) 13 | contract_.setProvider(web3.currentProvider) 14 | contracts[k] = contract_ 15 | } 16 | return contracts 17 | } 18 | -------------------------------------------------------------------------------- /app/reducers/ipfs.js: -------------------------------------------------------------------------------- 1 | const ipfs = (ipfsState = {}, action) => { 2 | // Pre-checks 3 | let state = ipfsState 4 | if (!action.info || !state.instance) { 5 | state = getIpfs(state) 6 | } 7 | if (!state.instance) return state 8 | 9 | // Handler for actions 10 | switch (action.type) { 11 | 12 | } 13 | return state 14 | } 15 | 16 | const getIpfs = (state) => { 17 | return (typeof window.ipfs === 'object') ? 18 | { ...state, instance: window.ipfs } : 19 | { ...state, error: 'No IPFS found!' } 20 | } 21 | 22 | export default ipfs 23 | -------------------------------------------------------------------------------- /app/init/index.js: -------------------------------------------------------------------------------- 1 | import { contractArtifacts, initialise } from './contracts' 2 | import { web3 } from './web3' 3 | // import { ipfs } from './storage' 4 | 5 | window.web3 = web3 6 | window.contracts = initialise(contractArtifacts, web3) 7 | // window.ipfs = ipfs 8 | window.ipfs = window.IpfsApi() 9 | 10 | if (module.hot) { 11 | let { contractArtifacts } = require('./contracts') 12 | // let { ipfs } = require('./storage') 13 | 14 | window.contracts = initialise(contractArtifacts, web3) 15 | // window.ipfs = ipfs 16 | window.ipfs = window.IpfsApi() 17 | } 18 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/actions/ipfs.js: -------------------------------------------------------------------------------- 1 | // Write file to IPFS 2 | export const IPFS_WRITE_FILE_PENDING = 'IPFS_WRITE_FILE_PENDING' 3 | export const IPFS_WRITE_FILE_SUCCESS = 'IPFS_WRITE_FILE_SUCCESS' 4 | export const IPFS_WRITE_FILE_ERROR = 'IPFS_WRITE_FILE_ERROR' 5 | 6 | export const ipfsWriteFile = () => (dispatch) => { 7 | dispatch(ipfsWriteFilePending()) 8 | 9 | } 10 | 11 | const ipfsWriteFilePending = () => { 12 | return { 13 | type: IPFS_WRITE_FILE_PENDING 14 | } 15 | } 16 | 17 | const ipfsWriteFileSuccess = (fileMetadata) => { 18 | return { 19 | type: IPFS_WRITE_FILE_SUCCESS, 20 | fileMetadata 21 | } 22 | } 23 | 24 | const ipfsWriteFileError = (error) => { 25 | return { 26 | type: IPFS_WRITE_FILE_ERROR, 27 | error 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/actions/accounts.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { web3 } from '../../app/init/web3' 4 | 5 | import { 6 | accountsInitPending, 7 | accountsInitSuccess 8 | } from '../../app/actions/accounts' 9 | 10 | describe('Actions: accounts', () => { 11 | it('should provide a blank account init pending', () => { 12 | const expected = { 13 | type: 'ACCOUNTS_INIT_PENDING' 14 | } 15 | expect(accountsInitPending()).to.deep.equal(expected) 16 | }) 17 | 18 | it('should provide a filled init success', () => { 19 | const accounts = [{ random: 1 }] 20 | const expected = { 21 | type: 'ACCOUNTS_INIT_SUCCESS', 22 | accounts 23 | } 24 | expect(accountsInitSuccess(accounts)).to.deep.equal(expected) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /app/reducers/accounts.js: -------------------------------------------------------------------------------- 1 | import { 2 | ACCOUNTS_INIT_PENDING, 3 | ACCOUNTS_INIT_SUCCESS, 4 | ACCOUNTS_INIT_ERROR 5 | } from '../actions' 6 | 7 | const accounts = (state = {}, action) => { 8 | switch (action.type) { 9 | case ACCOUNTS_INIT_SUCCESS: 10 | return handleAccountsInitSuccess(state, action.accounts) 11 | case ACCOUNTS_INIT_ERROR: 12 | return handleAccountsInitError(state, action.error) 13 | } 14 | return state 15 | } 16 | 17 | // Accounts has to have at least one value 18 | const handleAccountsInitSuccess = (state, accounts) => { 19 | return { 20 | ...state, 21 | all: accounts, 22 | default: accounts[0] 23 | } 24 | } 25 | 26 | const handleAccountsInitError = (state, error) => { 27 | return { 28 | ...state, 29 | error 30 | } 31 | } 32 | 33 | export default accounts 34 | -------------------------------------------------------------------------------- /app/style/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", sans-serif; 4 | } 5 | 6 | label { 7 | display: inline-block; 8 | width: 100px; 9 | } 10 | 11 | input { 12 | width: 500px; 13 | padding: 5px; 14 | font-size: 16px; 15 | } 16 | 17 | button { 18 | font-size: 16px; 19 | padding: 5px; 20 | } 21 | 22 | h1, h2 { 23 | display: block; 24 | vertical-align: middle; 25 | margin-top: 0px; 26 | margin-bottom: 10px; 27 | } 28 | 29 | h2 { 30 | color: #AAA; 31 | font-size: 32px; 32 | } 33 | 34 | h3 { 35 | font-weight: normal; 36 | color: #AAA; 37 | font-size: 24px; 38 | } 39 | 40 | .app { 41 | margin: auto; 42 | padding: 50px 0; 43 | max-width: 500px; 44 | } 45 | 46 | .black { 47 | color: black; 48 | } 49 | 50 | #balance { 51 | color: black; 52 | } 53 | 54 | .hint { 55 | color: #666; 56 | } 57 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | // Redux store 2 | import { applyMiddleware, compose, createStore } from 'redux' 3 | import thunk from 'redux-thunk' 4 | import logger from 'redux-logger' 5 | 6 | import reducers from './reducers' 7 | import { 8 | accountsInit 9 | } from './actions' 10 | 11 | let middleware = (process.env.NODE_ENV !== 'production') 12 | ? applyMiddleware(thunk, logger()) 13 | : applyMiddleware(thunk) 14 | 15 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 16 | 17 | const store = createStore(reducers, {}, composeEnhancer(middleware)) 18 | 19 | // Get accounts on store load 20 | store.dispatch(accountsInit()) 21 | 22 | if (module.hot) { 23 | // Enable Webpack hot module replacement for reducers 24 | module.hot.accept('./reducers', () => { 25 | const nextReducers = require('./reducers/index') 26 | store.replaceReducer(nextReducers) 27 | }) 28 | } 29 | 30 | export default store 31 | -------------------------------------------------------------------------------- /app/actions/accounts.js: -------------------------------------------------------------------------------- 1 | // Accounts initialise 2 | export const ACCOUNTS_INIT_PENDING = 'ACCOUNTS_INIT_PENDING' 3 | export const ACCOUNTS_INIT_SUCCESS = 'ACCOUNTS_INIT_SUCCESS' 4 | export const ACCOUNTS_INIT_ERROR = 'ACCOUNTS_INIT_ERROR' 5 | 6 | export const accountsInit = () => (dispatch) => { 7 | dispatch(accountsInitPending()) 8 | if (window.web3) { 9 | const web3 = window.web3 10 | web3.eth.getAccounts((err, accs) => { 11 | if (err != null) { 12 | dispatch(accountsInitError(err)) 13 | } else if (accs.length == 0) { 14 | dispatch(accountsInitError('No accounts found!')) 15 | } else { 16 | dispatch(accountsInitSuccess(accs)) 17 | } 18 | }) 19 | } else { 20 | dispatch(accountsInitError('No web3 object!')) 21 | } 22 | } 23 | 24 | export const accountsInitPending = () => { 25 | return { 26 | type: ACCOUNTS_INIT_PENDING 27 | } 28 | } 29 | 30 | export const accountsInitSuccess = (accounts) => { 31 | return { 32 | type: ACCOUNTS_INIT_SUCCESS, 33 | accounts 34 | } 35 | } 36 | 37 | export const accountsInitError = (error) => { 38 | return { 39 | type: ACCOUNTS_INIT_ERROR, 40 | error 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Truffle with Webpack Boilerplate 2 | 3 | [![Build Status](https://travis-ci.com/FreddieLindsey/truffle-webpack-boilerplate.svg?token=2txBfbss4toxp7qpR4fW&branch=master)](https://travis-ci.com/FreddieLindsey/truffle-webpack-boilerplate) 4 | 5 | ## Purpose 6 | 7 | There are quite a number of truffle boilerplates out there, but none that really completely worked out of the box. This is particularly the case with Truffle version 3.x. This boilerplate works completely with version 3.x as far as I know. 8 | 9 | Here, just run the following: 10 | 11 | ```bash 12 | yarn run services 13 | yarn start 14 | ``` 15 | 16 | This should get you going with a testrpc instance running on Docker. Starting yarn then brings up webpack which migrates and hot-reloads your contracts as you edit them. 17 | 18 | Have fun! 19 | 20 | ## Docker 21 | 22 | I couldn't find the Docker image referenced in the [testrpc repository](https://github.com/ethereumjs/testrpc). I have therefore built it and pushed it publicly to Docker. Please kindly recognise that this work is not my own, but that of [ethereumjs](https://github.com/ethereumjs). 23 | 24 | ## Issues 25 | 26 | Please feel free to contact me or submit a PR if you find any issues you'd like to share. 27 | -------------------------------------------------------------------------------- /app/reducers/contracts/SimpleStorage.js: -------------------------------------------------------------------------------- 1 | import { 2 | // SIMPLESTORAGE_ADDRESS_GET_PENDING, 3 | SIMPLESTORAGE_ADDRESS_GET_SUCCESS, 4 | SIMPLESTORAGE_ADDRESS_GET_ERROR, 5 | // SIMPLESTORAGE_VALUE_GET_PENDING, 6 | SIMPLESTORAGE_VALUE_GET_SUCCESS, 7 | SIMPLESTORAGE_VALUE_GET_ERROR, 8 | // SIMPLESTORAGE_VALUE_SET_PENDING, 9 | SIMPLESTORAGE_VALUE_SET_SUCCESS, 10 | SIMPLESTORAGE_VALUE_SET_ERROR 11 | } from '../../actions' 12 | 13 | const SimpleStorage = (state = {}, action) => { 14 | switch (action.type) { 15 | case SIMPLESTORAGE_ADDRESS_GET_SUCCESS: 16 | return handleAddressGetSuccess(state, action.address) 17 | case SIMPLESTORAGE_ADDRESS_GET_ERROR: 18 | return handleAddressGetError(state, action.error) 19 | case SIMPLESTORAGE_VALUE_GET_SUCCESS: 20 | return handleValueGetSuccess(state, action.value) 21 | case SIMPLESTORAGE_VALUE_GET_ERROR: 22 | return handleValueGetError(state, action.error) 23 | case SIMPLESTORAGE_VALUE_SET_SUCCESS: 24 | return handleValueSetSuccess(state, action.value) 25 | case SIMPLESTORAGE_VALUE_SET_ERROR: 26 | return handleValueSetError(state, action.error) 27 | } 28 | return state 29 | } 30 | 31 | const handleAddressGetSuccess = (state, address) => { 32 | return { 33 | ...state, 34 | error: undefined, 35 | address 36 | } 37 | } 38 | 39 | const handleAddressGetError = (state, error) => { 40 | return { 41 | ...state, 42 | error 43 | } 44 | } 45 | 46 | const handleValueGetSuccess = (state, value) => { 47 | return { 48 | ...state, 49 | error: undefined, 50 | value 51 | } 52 | } 53 | 54 | const handleValueGetError = (state, error) => { 55 | return { 56 | ...state, 57 | error 58 | } 59 | } 60 | 61 | const handleValueSetSuccess = (state, value) => { 62 | return { 63 | ...state, 64 | error: undefined, 65 | value 66 | } 67 | } 68 | 69 | const handleValueSetError = (state, error) => { 70 | return { 71 | ...state, 72 | error 73 | } 74 | } 75 | 76 | export default SimpleStorage 77 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0 */ 2 | 'use strict' 3 | 4 | var webpack = require('webpack') 5 | var path = require('path') 6 | var CopyWebpackPlugin = require('copy-webpack-plugin') 7 | 8 | module.exports = { 9 | cache: true, 10 | devtool: 'source-map', 11 | devServer: { 12 | stats: 'errors-only', 13 | }, 14 | entry: './app/index.js', 15 | output: { 16 | path: path.resolve(__dirname, 'build'), 17 | filename: 'app.js' 18 | }, 19 | resolve: { 20 | extensions: ['.js', '.jsx'] 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | enforce: 'pre', 26 | test: /\.js(x)?$/, 27 | loader: 'eslint-loader' 28 | }, 29 | { 30 | test: /\.js(x)?$/, 31 | exclude: /(node_modules)/, 32 | loader: 'babel-loader' 33 | }, 34 | { 35 | test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/, 36 | loader: 'url-loader' 37 | }, 38 | { 39 | test: /\.json$/, 40 | loader: 'json-loader' 41 | }, 42 | { 43 | test: /\.(s)?css$/, 44 | use: [ 45 | { 46 | loader: 'style-loader' 47 | }, { 48 | loader: 'css-loader', 49 | options: { 50 | camelCase: true, 51 | importLoaders: 1, 52 | localIdentName: '[path][name]__[local]--[hash:base64:5]', 53 | modules: true, 54 | sourceMap: true 55 | } 56 | }, { 57 | loader: 'sass-loader', 58 | options: { sourceMap: true } 59 | } 60 | ] 61 | }, 62 | { 63 | test: /\.(png|jpg)$/, 64 | loader: 'url-loader?limit=8192' 65 | }, 66 | { 67 | test: /\.sol/, 68 | loaders: ['json-loader', 'truffle-solidity-loader?network=development'] 69 | } 70 | ], 71 | noParse: /lie\.js|[\s\S]*.(svg|ttf|eot)/ 72 | }, 73 | node: { 74 | fs: 'empty', 75 | net: 'empty', 76 | tls: 'empty' 77 | }, 78 | plugins: [ 79 | // Copy our app's index.html to the build folder. 80 | new CopyWebpackPlugin([ 81 | { from: './app/index.html', to: 'index.html' } 82 | ]), 83 | new webpack.NoEmitOnErrorsPlugin(), 84 | new webpack.DefinePlugin({ 85 | '__DEV__': true, 86 | 'process.env': { 87 | 'NODE_ENV': JSON.stringify('development') 88 | } 89 | }) 90 | ], 91 | stats: { 92 | children: false 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "browser": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "globals": { 10 | "React": true 11 | }, 12 | "plugins": [ 13 | "react" 14 | ], 15 | "ecmaFeatures": { 16 | "arrowFunctions": true, 17 | "blockBindings": true, 18 | "classes": true, 19 | "defaultParams": true, 20 | "destructuring": true, 21 | "forOf": true, 22 | "generators": false, 23 | "modules": true, 24 | "objectLiteralComputedProperties": true, 25 | "objectLiteralDuplicateProperties": false, 26 | "objectLiteralShorthandMethods": true, 27 | "objectLiteralShorthandProperties": true, 28 | "spread": true, 29 | "superInFunctions": true, 30 | "templateStrings": true, 31 | "jsx": true 32 | }, 33 | "rules": { 34 | "no-bitwise": 1, 35 | "indent": [2, 2, { "SwitchCase": 1 }], 36 | "max-depth": [1, 4], 37 | "max-len": ["error", 100], 38 | "max-statements": [1, 30], 39 | "complexity": [1, 30], 40 | "quotes": 0, 41 | "eol-last": 1, 42 | "dot-notation": 0, 43 | "strict": [2, "never"], 44 | "react/display-name": 1, 45 | "react/jsx-boolean-value": 2, 46 | "react/jsx-no-undef": 2, 47 | "react/jsx-sort-props": 0, 48 | "react/jsx-sort-prop-types": 0, 49 | "react/jsx-uses-react": 2, 50 | "react/jsx-uses-vars": 2, 51 | "react/no-did-mount-set-state": 2, 52 | "react/no-did-update-set-state": 2, 53 | "react/no-multi-comp": 2, 54 | "react/no-unknown-property": 2, 55 | "react/prop-types": 2, 56 | "react/react-in-jsx-scope": 2, 57 | "react/self-closing-comp": 2, 58 | "react/jsx-wrap-multilines": 2, 59 | "react/sort-comp": [2, { 60 | "order": [ 61 | "displayName", 62 | "propTypes", 63 | "contextTypes", 64 | "childContextTypes", 65 | "mixins", 66 | "statics", 67 | "defaultProps", 68 | "constructor", 69 | "getDefaultProps", 70 | "getInitialState", 71 | "getChildContext", 72 | "componentWillMount", 73 | "componentDidMount", 74 | "componentWillReceiveProps", 75 | "shouldComponentUpdate", 76 | "componentWillUpdate", 77 | "componentDidUpdate", 78 | "componentWillUnmount", 79 | "/^on.+$/", 80 | "/^get.+$/", 81 | "/^handle.+$/", 82 | "everything-else", 83 | "/^render.+$/", 84 | "render" 85 | ] 86 | }] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truffle_boilerplate", 3 | "version": "0.0.1", 4 | "description": "Frontend with tested contracts using webpack", 5 | "scripts": { 6 | "lint": "eslint ./", 7 | "build": "webpack", 8 | "build-docker": "./scripts/build_docker.sh", 9 | "clean": "rm -rf .node-xml* node_modules", 10 | "logs-ipfs": "docker logs ipfs --follow", 11 | "logs-testrpc": "docker logs testrpc --follow", 12 | "services": "./scripts/start_services.sh", 13 | "start": "webpack-dev-server --host 0.0.0.0 --port 3000 --hot --content-base build", 14 | "test": "yarn run test-contracts; yarn run test-web", 15 | "test-contracts": "truffle test", 16 | "test-web": "mocha --compilers js:babel-register spec/**/*.spec.js" 17 | }, 18 | "author": "Frederick Lindsey", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "babel-cli": "^6.22.2", 22 | "babel-core": "^6.22.1", 23 | "babel-eslint": "^6.1.2", 24 | "babel-loader": "^6.2.10", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-polyfill": "^6.23.0", 27 | "babel-preset-env": "^1.1.8", 28 | "babel-preset-latest": "^6.22.0", 29 | "babel-preset-react": "^6.23.0", 30 | "babel-preset-react-hmre": "^1.1.1", 31 | "babel-preset-stage-0": "^6.22.0", 32 | "babel-register": "^6.23.0", 33 | "chai": "^3.5.0", 34 | "copy-webpack-plugin": "^4.0.1", 35 | "css-loader": "^0.26.1", 36 | "eslint": "^3.14.0", 37 | "eslint-config-standard": "^6.0.0", 38 | "eslint-loader": "^1.6.3", 39 | "eslint-plugin-babel": "^4.0.0", 40 | "eslint-plugin-mocha": "^4.8.0", 41 | "eslint-plugin-promise": "^3.0.0", 42 | "eslint-plugin-react": "^6.10.0", 43 | "eslint-plugin-standard": "^2.0.0", 44 | "html-webpack-plugin": "^2.28.0", 45 | "ipfs-api": "^12.1.7", 46 | "json-loader": "^0.5.4", 47 | "mocha": "^3.2.0", 48 | "node-sass": "^4.5.0", 49 | "react": "^15.4.2", 50 | "react-dom": "^15.4.2", 51 | "react-redux": "^5.0.3", 52 | "redux": "^3.6.0", 53 | "redux-logger": "^2.8.1", 54 | "redux-thunk": "^2.2.0", 55 | "sass-loader": "^6.0.2", 56 | "style-loader": "^0.13.1", 57 | "truffle": "^3.2.4", 58 | "truffle-contract": "^1.1.6", 59 | "truffle-solidity-loader": "git+https://github.com/sogoiii/truffle-solidity-loader.git#1f1e213d52f033b6863218307b8968ae68220fe1", 60 | "web3": "^0.18.2", 61 | "webpack": "^2.2.1", 62 | "webpack-dev-server": "^2.4.1" 63 | }, 64 | "dependencies": { 65 | "ethereumjs-testrpc": "^3.0.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import Web3 from 'web3' 4 | 5 | import { 6 | simpleStorageAddressGet, 7 | simpleStorageValueGet, 8 | simpleStorageValueSet 9 | } from '../../actions' 10 | 11 | const mapStateToProps = (state) => { 12 | return { 13 | accounts: state.accounts, 14 | SimpleStorage: state.SimpleStorage 15 | } 16 | } 17 | 18 | const mapDispatchToProps = (dispatch) => { 19 | return { 20 | handleAddressGet: () => simpleStorageAddressGet(dispatch), 21 | handleValueGet: () => simpleStorageValueGet(dispatch), 22 | handleValueSet: (v, a) => simpleStorageValueSet(dispatch, v, a) 23 | } 24 | } 25 | 26 | class App extends Component { 27 | 28 | static displayName = 'App' 29 | static propTypes = { 30 | accounts: PropTypes.shape({ 31 | all: PropTypes.arrayOf(PropTypes.string), 32 | default: PropTypes.string 33 | }).isRequired, 34 | SimpleStorage: PropTypes.shape({ 35 | address: PropTypes.string, 36 | value: PropTypes.any 37 | }).isRequired, 38 | 39 | handleAddressGet: PropTypes.func.isRequired, 40 | handleValueGet: PropTypes.func.isRequired, 41 | handleValueSet: PropTypes.func.isRequired 42 | } 43 | 44 | componentDidMount () { 45 | this.props.handleAddressGet() 46 | this.props.handleValueGet() 47 | } 48 | 49 | handleValueSet () { 50 | let newValue = this.textInput.value 51 | if (!newValue || newValue === '') return 52 | this.props.handleValueSet(newValue, this.props.accounts.default) 53 | } 54 | 55 | render () { 56 | const { 57 | address, 58 | value 59 | } = this.props.SimpleStorage 60 | return ( 61 |
62 |

Simple Storage

63 | 64 | { address ? 65 |
66 |

67 | SimpleStorage deployed at { address } 68 |

69 | 70 | { value && 71 |
72 |
73 |
74 | Current value: { value.toString() } 75 |
76 | } 77 | 78 | { value ? 79 |
80 | { this.textInput = i }} /> 81 | 84 |
85 | : 86 | 89 | } 90 |
91 | : 92 | 95 | } 96 | 97 |
98 |
99 | 100 | 101 |

Hint:

102 | Open the browser developer console to 103 | view redux state changes, errors and warnings. 104 |
105 |
106 | ) 107 | } 108 | 109 | } 110 | 111 | export default connect(mapStateToProps, mapDispatchToProps)(App) 112 | -------------------------------------------------------------------------------- /app/actions/contracts/SimpleStorage.js: -------------------------------------------------------------------------------- 1 | // Get Address 2 | export const SIMPLESTORAGE_ADDRESS_GET_PENDING = 'SIMPLESTORAGE_ADDRESS_GET_PENDING' 3 | export const SIMPLESTORAGE_ADDRESS_GET_SUCCESS = 'SIMPLESTORAGE_ADDRESS_GET_SUCCESS' 4 | export const SIMPLESTORAGE_ADDRESS_GET_ERROR = 'SIMPLESTORAGE_ADDRESS_GET_ERROR' 5 | 6 | export const simpleStorageAddressGet = (dispatch) => { 7 | dispatch(simpleStorageAddressGetPending()) 8 | window.contracts.SimpleStorage.deployed() 9 | .then((instance) => { 10 | dispatch(simpleStorageAddressGetSuccess(instance.address)) 11 | }) 12 | .catch((err) => { 13 | dispatch(simpleStorageAddressGetError(err)) 14 | }) 15 | } 16 | 17 | const simpleStorageAddressGetPending = () => { 18 | return { 19 | type: SIMPLESTORAGE_ADDRESS_GET_PENDING 20 | } 21 | } 22 | 23 | const simpleStorageAddressGetSuccess = (address) => { 24 | return { 25 | type: SIMPLESTORAGE_ADDRESS_GET_SUCCESS, 26 | address 27 | } 28 | } 29 | 30 | const simpleStorageAddressGetError = (error) => { 31 | return { 32 | type: SIMPLESTORAGE_ADDRESS_GET_ERROR, 33 | error 34 | } 35 | } 36 | 37 | // Get Value 38 | export const SIMPLESTORAGE_VALUE_GET_PENDING = 'SIMPLESTORAGE_VALUE_GET_PENDING' 39 | export const SIMPLESTORAGE_VALUE_GET_SUCCESS = 'SIMPLESTORAGE_VALUE_GET_SUCCESS' 40 | export const SIMPLESTORAGE_VALUE_GET_ERROR = 'SIMPLESTORAGE_VALUE_GET_ERROR' 41 | 42 | export const simpleStorageValueGet = (dispatch) => { 43 | dispatch(simpleStorageValueGetPending()) 44 | window.contracts.SimpleStorage.deployed() 45 | .then((instance) => { 46 | return instance.get() 47 | }) 48 | .then((value) => { 49 | dispatch(simpleStorageValueGetSuccess(value)) 50 | }) 51 | .catch((error) => { 52 | dispatch(simpleStorageValueGetError(error)) 53 | }) 54 | } 55 | 56 | const simpleStorageValueGetPending = () => { 57 | return { 58 | type: SIMPLESTORAGE_VALUE_GET_PENDING 59 | } 60 | } 61 | 62 | const simpleStorageValueGetSuccess = (value) => { 63 | return { 64 | type: SIMPLESTORAGE_VALUE_GET_SUCCESS, 65 | value 66 | } 67 | } 68 | 69 | const simpleStorageValueGetError = (error) => { 70 | return { 71 | type: SIMPLESTORAGE_VALUE_GET_ERROR, 72 | error 73 | } 74 | } 75 | 76 | // Get Value 77 | export const SIMPLESTORAGE_VALUE_SET_PENDING = 'SIMPLESTORAGE_VALUE_SET_PENDING' 78 | export const SIMPLESTORAGE_VALUE_SET_SUCCESS = 'SIMPLESTORAGE_VALUE_SET_SUCCESS' 79 | export const SIMPLESTORAGE_VALUE_SET_ERROR = 'SIMPLESTORAGE_VALUE_SET_ERROR' 80 | 81 | export const simpleStorageValueSet = (dispatch, value, address) => { 82 | dispatch(simpleStorageValueSetPending()) 83 | window.contracts.SimpleStorage.deployed() 84 | .then((instance) => { 85 | return instance.set(value, { from: address }) 86 | }) 87 | .then((receipt) => { 88 | console.dir(receipt) 89 | dispatch(simpleStorageValueSetSuccess(value)) 90 | }) 91 | .catch((error) => { 92 | dispatch(simpleStorageValueSetError(error)) 93 | }) 94 | } 95 | 96 | const simpleStorageValueSetPending = () => { 97 | return { 98 | type: SIMPLESTORAGE_VALUE_SET_PENDING 99 | } 100 | } 101 | 102 | const simpleStorageValueSetSuccess = (value) => { 103 | return { 104 | type: SIMPLESTORAGE_VALUE_SET_SUCCESS, 105 | value 106 | } 107 | } 108 | 109 | const simpleStorageValueSetError = (error) => { 110 | return { 111 | type: SIMPLESTORAGE_VALUE_SET_ERROR, 112 | error 113 | } 114 | } 115 | --------------------------------------------------------------------------------