├── src ├── containers │ ├── appContainer.css │ ├── devTools.js │ ├── appContainer.js │ ├── homeContainer.js │ └── projectContainer.js ├── index.css ├── fonts │ ├── Oswald-300 │ │ ├── Oswald-300.eot │ │ ├── Oswald-300.ttf │ │ ├── Oswald-300.woff │ │ ├── Oswald-300.woff2 │ │ └── LICENSE.txt │ ├── Oswald-regular │ │ ├── Oswald-regular.eot │ │ ├── Oswald-regular.ttf │ │ ├── Oswald-regular.woff │ │ ├── Oswald-regular.woff2 │ │ └── LICENSE.txt │ └── Open-Sans-regular │ │ ├── Open-Sans-regular.eot │ │ ├── Open-Sans-regular.ttf │ │ ├── Open-Sans-regular.woff │ │ ├── Open-Sans-regular.woff2 │ │ └── LICENSE.txt ├── components │ ├── projectCard.css │ ├── contributionList.js │ ├── navigation.js │ ├── projectCard.js │ ├── projectList.js │ ├── projectDetails.js │ ├── contributeModal.js │ └── createProjectModal.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ ├── configureStore.dev.js │ └── middlewares │ │ └── apiMiddleware.js ├── utils │ ├── textUtils.js │ ├── projectUtils.js │ └── web3Utils.js ├── config │ └── routes.js ├── reducers │ ├── rootReducer.js │ ├── userReducer.js │ ├── networkReducer.js │ ├── reducerUtil.js │ ├── fundingHubReducer.js │ └── projectReducer.js ├── css │ ├── open-sans.css │ ├── oswald.css │ └── pure-min.css ├── actions │ ├── userActions.js │ ├── networkActions.js │ ├── fundingHubActions.js │ └── projectActions.js ├── index.js └── api │ └── web3Api.js ├── public ├── favicon.ico ├── project.png ├── create_project.png ├── funding_hub_screen.png └── index.html ├── .gitignore ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── contracts ├── ConvertLib.sol ├── Migrations.sol ├── FundingHub.sol └── Project.sol ├── docker-compose.yml ├── truffle.js ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── truffle-config.js ├── test ├── TestMetacoin.sol └── metacoin.js ├── scripts ├── test.js ├── build.js └── start.js ├── nginx.conf ├── package.json └── README.md /src/containers/appContainer.css: -------------------------------------------------------------------------------- 1 | 2 | .mainContent { 3 | padding-top: 70px; 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/public/project.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tern-port 2 | .DS_Store 3 | node_modules/ 4 | *.log 5 | /build 6 | .truffle-solidity-loader 7 | -------------------------------------------------------------------------------- /public/create_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/public/create_project.png -------------------------------------------------------------------------------- /public/funding_hub_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/public/funding_hub_screen.png -------------------------------------------------------------------------------- /src/fonts/Oswald-300/Oswald-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-300/Oswald-300.eot -------------------------------------------------------------------------------- /src/fonts/Oswald-300/Oswald-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-300/Oswald-300.ttf -------------------------------------------------------------------------------- /src/fonts/Oswald-300/Oswald-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-300/Oswald-300.woff -------------------------------------------------------------------------------- /src/fonts/Oswald-300/Oswald-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-300/Oswald-300.woff2 -------------------------------------------------------------------------------- /src/fonts/Oswald-regular/Oswald-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-regular/Oswald-regular.eot -------------------------------------------------------------------------------- /src/fonts/Oswald-regular/Oswald-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-regular/Oswald-regular.ttf -------------------------------------------------------------------------------- /src/fonts/Oswald-regular/Oswald-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-regular/Oswald-regular.woff -------------------------------------------------------------------------------- /src/fonts/Oswald-regular/Oswald-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Oswald-regular/Oswald-regular.woff2 -------------------------------------------------------------------------------- /src/fonts/Open-Sans-regular/Open-Sans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Open-Sans-regular/Open-Sans-regular.eot -------------------------------------------------------------------------------- /src/fonts/Open-Sans-regular/Open-Sans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Open-Sans-regular/Open-Sans-regular.ttf -------------------------------------------------------------------------------- /src/fonts/Open-Sans-regular/Open-Sans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Open-Sans-regular/Open-Sans-regular.woff -------------------------------------------------------------------------------- /src/fonts/Open-Sans-regular/Open-Sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyndallm/crowdfund-dapp/HEAD/src/fonts/Open-Sans-regular/Open-Sans-regular.woff2 -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var FundingHub = artifacts.require("./FundingHub.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(FundingHub); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/ConvertLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | library ConvertLib{ 3 | function convert(uint amount,uint conversionRate) returns (uint convertedAmount) 4 | { 5 | return amount * conversionRate; 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/projectCard.css: -------------------------------------------------------------------------------- 1 | .projectCard { 2 | padding-top: 48px !important; 3 | padding-bottom: 48px; 4 | padding-left: 60px; 5 | padding-right: 60px; 6 | background-color: #F2F3F5; 7 | height: 100%; 8 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This Docker Compose config will get a full static site up and running on nginx 2 | # quickly so you can test out how the site will run on a real server. 3 | static: 4 | image: nginx:1.9 5 | ports: 6 | - "8080:80" 7 | - "8443:443" 8 | volumes: 9 | - "./nginx.conf:/etc/nginx/nginx.conf" 10 | - "./build_webpack:/var/www/html" -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | migrations_directory: "./migrations", 3 | networks: { 4 | development: { 5 | host: "localhost", 6 | port: 8545, 7 | network_id: "*" // Match any network id 8 | }, 9 | testnet: { 10 | host: "localhost", 11 | port: 8545, 12 | network_id: 3 // Ropsten 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // This is a custom Jest transformer turning file imports into filenames. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | migrations_directory: "./migrations", 3 | networks: { 4 | development: { 5 | host: "localhost", 6 | port: 8545, 7 | network_id: "*" // Match any network id 8 | }, 9 | testnet: { 10 | host: "localhost", 11 | port: 8545, 12 | network_id: 3 // Ropsten 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import {configureStore as devStore} from './configureStore.dev'; 2 | import {configureStore as prodStore} from './configureStore.prod'; 3 | 4 | export function configureStore(history) { 5 | if (process.env.NODE_ENV === 'production') { 6 | return prodStore(history); 7 | } else { 8 | return devStore(history); 9 | } 10 | } -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return 'module.exports = {};'; 7 | }, 8 | getCacheKey(fileData, filename) { 9 | // The output is always the same. 10 | return 'cssTransform'; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/textUtils.js: -------------------------------------------------------------------------------- 1 | import { fromWei } from '../api/web3Api'; 2 | 3 | export function getAccountString(account) { 4 | return account.address + " (" + fromWei(account.balance) + " ETH)" 5 | } 6 | 7 | export function getFormattedUserAccount(account) { 8 | let userBalance = parseFloat(fromWei(account.balance)).toFixed(3); 9 | return `${account.address} (${userBalance} ETH)`; 10 | } 11 | -------------------------------------------------------------------------------- /src/containers/devTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import DockMonitor from 'redux-devtools-dock-monitor'; 4 | import Inspector from 'redux-devtools-inspector'; 5 | 6 | const DevTools = createDevTools( 7 | 11 | 12 | 13 | ); 14 | 15 | export default DevTools; -------------------------------------------------------------------------------- /src/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | import AppContainer from '../containers/appContainer'; 4 | import HomeContainer from '../containers/homeContainer'; 5 | import ProjectContainer from '../containers/projectContainer'; 6 | 7 | export default () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | ) 14 | } -------------------------------------------------------------------------------- /src/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import {routerReducer} from 'react-router-redux'; 3 | 4 | import {userReducer} from './userReducer'; 5 | import {networkReducer} from './networkReducer'; 6 | import {fundingHubReducer} from './fundingHubReducer'; 7 | import {projectReducer} from './projectReducer'; 8 | 9 | const rootReducer = combineReducers({ 10 | user: userReducer, 11 | routing: routerReducer, 12 | network: networkReducer, 13 | fundingHub: fundingHubReducer, 14 | project: projectReducer 15 | }); 16 | 17 | export default rootReducer; 18 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | contract Migrations { 3 | address public owner; 4 | uint public last_completed_migration; 5 | 6 | modifier restricted() { 7 | if (msg.sender == owner) _; 8 | } 9 | 10 | function Migrations() { 11 | owner = msg.sender; 12 | } 13 | 14 | function setCompleted(uint completed) restricted { 15 | last_completed_migration = completed; 16 | } 17 | 18 | function upgrade(address new_address) restricted { 19 | Migrations upgraded = Migrations(new_address); 20 | upgraded.setCompleted(last_completed_migration); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import {routerMiddleware} from 'react-router-redux'; 4 | import {apiMiddleware} from './middlewares/apiMiddleware'; 5 | import rootReducer from '../reducers/rootReducer'; 6 | 7 | 8 | export function configureStore(history) { 9 | const routingMiddleware = routerMiddleware(history); 10 | 11 | const middlewares = applyMiddleware( 12 | thunkMiddleware, 13 | routingMiddleware, 14 | apiMiddleware 15 | ); 16 | 17 | const initialState = {}; 18 | 19 | return createStore(rootReducer, initialState, middlewares); 20 | } -------------------------------------------------------------------------------- /src/css/open-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/Open-Sans-regular/Open-Sans-regular.eot'); 6 | src: url('../fonts/Open-Sans-regular/Open-Sans-regular.eot?#iefix') format('embedded-opentype'), 7 | local('Open Sans'), 8 | local('Open-Sans-regular'), 9 | url('../fonts/Open-Sans-regular/Open-Sans-regular.woff2') format('woff2'), 10 | url('../fonts/Open-Sans-regular/Open-Sans-regular.woff') format('woff'), 11 | url('../fonts/Open-Sans-regular/Open-Sans-regular.ttf') format('truetype'), 12 | url('../fonts/Open-Sans-regular/Open-Sans-regular.svg#OpenSans') format('svg'); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/projectUtils.js: -------------------------------------------------------------------------------- 1 | 2 | export function getFormattedProgressPercentage(fundingRaised, fundingGoal) { 3 | return Number(((Number(fundingRaised) / Number(fundingGoal)) * 100).toFixed(2)); 4 | } 5 | 6 | export function getProjectStatus(currentBlock, deadlineBlock, fundsRaised, fundingGoal) { 7 | let status = "-"; 8 | 9 | if (currentBlock > deadlineBlock) { 10 | if (fundsRaised >= fundingGoal) { 11 | status = "Funded"; 12 | } else { 13 | status = "Failed"; 14 | } 15 | } else { 16 | if (fundsRaised >= fundingGoal) { 17 | status = "Funded"; 18 | } else { 19 | status = "Active"; 20 | } 21 | } 22 | return status; 23 | } -------------------------------------------------------------------------------- /test/TestMetacoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/MetaCoin.sol"; 6 | 7 | contract TestMetacoin { 8 | 9 | function testInitialBalanceUsingDeployedContract() { 10 | MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin()); 11 | 12 | uint expected = 10000; 13 | 14 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 15 | } 16 | 17 | function testInitialBalanceWithNewMetaCoin() { 18 | MetaCoin meta = new MetaCoin(); 19 | 20 | uint expected = 10000; 21 | 22 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import * as Web3Api from '../api/web3Api'; 2 | import {createAction} from 'redux-actions'; 3 | 4 | export const fetchAccountsRequest = "@@user/FETCH_ACCOUNTS_REQUEST"; 5 | export const fetchAccountsSuccess = "@@user/FETCH_ACCOUNTS_SUCCESS"; 6 | export const fetchAccountFailure = "@@user/FETCH_ACCOUNTS_FAILURE"; 7 | export const selectAccount = "@@user/SELECT_ACCOUNT"; 8 | 9 | export function fetchAccountsAndBalances() { 10 | return { 11 | types: [ 12 | fetchAccountsRequest, 13 | fetchAccountsSuccess, 14 | fetchAccountFailure, 15 | ], 16 | callApi: () => Web3Api.getAccounts(), 17 | payload: {} 18 | }; 19 | } 20 | 21 | export const setSelectedAccount = createAction(selectAccount); -------------------------------------------------------------------------------- /src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import {handleActions} from 'redux-actions'; 2 | 3 | import { 4 | fetchAccountsRequest, 5 | fetchAccountsSuccess, 6 | fetchAccountsFailure, 7 | selectAccount, 8 | } from '../actions/userActions'; 9 | 10 | import { 11 | requestReducer, 12 | fetchAccountsSuccessReducer, 13 | failureReducer, 14 | } from './reducerUtil'; 15 | 16 | const initialState = { 17 | isFetching: false, 18 | accounts: [], 19 | coinbase: "", 20 | selectedAccount: 0, 21 | } 22 | 23 | const setSelectedAccount = (state, action) => { 24 | return Object.assign({}, state, { 25 | selectedAccount: action.payload, 26 | }); 27 | }; 28 | 29 | export const userReducer = handleActions({ 30 | [fetchAccountsRequest]: requestReducer, 31 | [fetchAccountsSuccess]: fetchAccountsSuccessReducer, 32 | [fetchAccountsFailure]: failureReducer, 33 | [selectAccount]: setSelectedAccount 34 | }, initialState); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Router, browserHistory} from 'react-router'; 4 | import {Provider} from 'react-redux'; 5 | import {syncHistoryWithStore} from 'react-router-redux'; 6 | import {configureStore} from './store/configureStore'; 7 | import getRoutes from './config/routes'; 8 | import DevTools from './containers/devTools'; 9 | 10 | const store = configureStore(browserHistory); 11 | const history = syncHistoryWithStore(browserHistory, store); 12 | 13 | const devTools = () => { 14 | if (process.env.NODE_ENV === "production") { 15 | return null; 16 | } else { 17 | return ; 18 | } 19 | }; 20 | 21 | ReactDOM.render( 22 | 23 |
24 | 25 | {getRoutes()} 26 | 27 | {devTools()} 28 |
29 |
, 30 | document.getElementById('root') 31 | ); 32 | -------------------------------------------------------------------------------- /src/reducers/networkReducer.js: -------------------------------------------------------------------------------- 1 | import {handleActions} from 'redux-actions'; 2 | 3 | import { 4 | fetchNetworkRequest, 5 | fetchNetworkSuccess, 6 | fetchNetworkFailure, 7 | fetchBlockNumberRequest, 8 | fetchBlockNumberSuccess, 9 | fetchBlockNumberFailure, 10 | } from '../actions/networkActions'; 11 | 12 | import { 13 | requestReducer, 14 | fetchNetworkSuccessReducer, 15 | fetchBlockNumberSuccessReducer, 16 | failureReducer, 17 | } from './reducerUtil'; 18 | 19 | const initialState = { 20 | isFetching: false, 21 | network: "", 22 | currentBlock: 0, 23 | } 24 | 25 | export const networkReducer = handleActions({ 26 | [fetchNetworkRequest]: requestReducer, 27 | [fetchNetworkSuccess]: fetchNetworkSuccessReducer, 28 | [fetchNetworkFailure]: failureReducer, 29 | [fetchBlockNumberRequest]: requestReducer, 30 | [fetchBlockNumberSuccess]: fetchBlockNumberSuccessReducer, 31 | [fetchBlockNumberFailure]: failureReducer, 32 | }, initialState); -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI or in coverage mode 14 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 15 | argv.push('--watch'); 16 | } 17 | 18 | // A temporary hack to clear terminal correctly. 19 | // You can remove this after updating to Jest 18 when it's out. 20 | // https://github.com/facebook/jest/pull/2230 21 | var realWrite = process.stdout.write; 22 | var CLEAR = process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H'; 23 | process.stdout.write = function(chunk, encoding, callback) { 24 | if (chunk === '\x1B[2J\x1B[H') { 25 | chunk = CLEAR; 26 | } 27 | return realWrite.call(this, chunk, encoding, callback); 28 | }; 29 | 30 | 31 | jest.run(argv); 32 | -------------------------------------------------------------------------------- /src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import {routerMiddleware} from 'react-router-redux'; 4 | import {apiMiddleware} from './middlewares/apiMiddleware'; 5 | import DevTools from '../containers/devTools'; 6 | import rootReducer from '../reducers/rootReducer'; 7 | 8 | // Redux DevTools 9 | // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 10 | 11 | export function configureStore(history) { 12 | const routingMiddleware = routerMiddleware(history); 13 | 14 | const middlewares = applyMiddleware( 15 | thunkMiddleware, 16 | routingMiddleware, 17 | apiMiddleware 18 | ); 19 | 20 | const enhancer = compose( 21 | middlewares, 22 | DevTools.instrument() 23 | ); 24 | 25 | const initialState = {}; 26 | 27 | const store = createStore(rootReducer, initialState, enhancer); 28 | 29 | if (module["hot"]) { 30 | module["hot"].accept("../reducers/rootReducer", () => { 31 | return store.replaceReducer(rootReducer); 32 | }); 33 | } 34 | 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | var processEnv = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env[key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'NODE_ENV': JSON.stringify( 17 | process.env.NODE_ENV || 'development' 18 | ), 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': JSON.stringify(publicUrl) 24 | }); 25 | return {'process.env': processEnv}; 26 | } 27 | 28 | module.exports = getClientEnvironment; 29 | -------------------------------------------------------------------------------- /src/actions/networkActions.js: -------------------------------------------------------------------------------- 1 | import * as Web3Api from '../api/web3Api'; 2 | import {createAction} from 'redux-actions'; 3 | 4 | export const fetchNetworkRequest = "@@network/FETCH_NETWORK_REQUEST"; 5 | export const fetchNetworkSuccess = "@@network/FETCH_NETWORK_SUCCESS"; 6 | export const fetchNetworkFailure = "@@network/FETCH_NETWORK_FAILURE"; 7 | 8 | export const fetchBlockNumberRequest = "@@network/FETCH_BLOCK_NUMBER_REQUEST"; 9 | export const fetchBlockNumberSuccess = "@@network/FETCH_BLOCK_NUMBER_SUCCESS"; 10 | export const fetchBlockNumberFailure = "@@network/FETCH_BLOCK_NUMBER_FAILURE"; 11 | 12 | export function fetchNetwork() { 13 | return { 14 | types: [ 15 | fetchNetworkRequest, 16 | fetchNetworkSuccess, 17 | fetchNetworkFailure, 18 | ], 19 | callApi: () => Web3Api.getNetwork(), 20 | payload: {} 21 | }; 22 | } 23 | 24 | export function fetchBlockNumber() { 25 | return { 26 | types: [ 27 | fetchBlockNumberRequest, 28 | fetchBlockNumberSuccess, 29 | fetchBlockNumberFailure, 30 | ], 31 | callApi: () => Web3Api.getCurrentBlockNumber(), 32 | payload: {} 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/css/oswald.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Oswald'; 3 | font-weight: 300; 4 | font-style: normal; 5 | src: url('../fonts/Oswald-300/Oswald-300.eot'); 6 | src: url('../fonts/Oswald-300/Oswald-300.eot?#iefix') format('embedded-opentype'), 7 | local('Oswald Light'), 8 | local('Oswald-300'), 9 | url('../fonts/Oswald-300/Oswald-300.woff2') format('woff2'), 10 | url('../fonts/Oswald-300/Oswald-300.woff') format('woff'), 11 | url('../fonts/Oswald-300/Oswald-300.ttf') format('truetype'), 12 | url('../fonts/Oswald-300/Oswald-300.svg#Oswald') format('svg'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Oswald'; 17 | font-weight: 400; 18 | font-style: normal; 19 | src: url('../fonts/Oswald-regular/Oswald-regular.eot'); 20 | src: url('../fonts/Oswald-regular/Oswald-regular.eot?#iefix') format('embedded-opentype'), 21 | local('Oswald Regular'), 22 | local('Oswald-regular'), 23 | url('../fonts/Oswald-regular/Oswald-regular.woff2') format('woff2'), 24 | url('../fonts/Oswald-regular/Oswald-regular.woff') format('woff'), 25 | url('../fonts/Oswald-regular/Oswald-regular.ttf') format('truetype'), 26 | url('../fonts/Oswald-regular/Oswald-regular.svg#Oswald') format('svg'); 27 | } 28 | -------------------------------------------------------------------------------- /src/store/middlewares/apiMiddleware.js: -------------------------------------------------------------------------------- 1 | import {createAction} from "redux-actions"; 2 | 3 | export function apiMiddleware({ dispatch, getState }) { 4 | return next => action => { 5 | const { 6 | types, 7 | callApi, 8 | shouldCallApi = (_state) => true, 9 | payload = {} 10 | } = action; 11 | 12 | if (!types) { 13 | // Normal action: pass it on 14 | return next(action); 15 | } 16 | 17 | if ( 18 | !Array.isArray(types) || !types.every(type => typeof type === "string") 19 | ) { 20 | throw new Error("Expected an array of three string types."); 21 | } 22 | 23 | if (typeof callApi !== "function") { 24 | throw new Error("Expected fetch to be a function."); 25 | } 26 | 27 | if (!shouldCallApi(getState())) { 28 | return; 29 | } 30 | 31 | const [ requestType, successType, failureType ] = types; 32 | 33 | dispatch(createAction(requestType)(payload)); 34 | 35 | return callApi().then( 36 | response => dispatch(createAction(successType)(response)) 37 | ).catch(error => { 38 | dispatch(createAction(failureType)(error)); 39 | }); 40 | }; 41 | } -------------------------------------------------------------------------------- /src/reducers/reducerUtil.js: -------------------------------------------------------------------------------- 1 | 2 | export const requestReducer = (state, action) => { 3 | return Object.assign({}, state, { 4 | isFetching: true, 5 | fetchComplete: false, 6 | }); 7 | } 8 | 9 | const createSuccessReducer = (property) => (state, action) => { 10 | return Object.assign({}, state, { 11 | isFetching: false, 12 | fetchComplete: true, 13 | [property]: action.payload 14 | }); 15 | } 16 | 17 | export const failureReducer = (state, action) => { 18 | console.log("Error: ", action.payload); 19 | return Object.assign({}, state, { 20 | isFetching: false, 21 | fetchComplete: true, 22 | error: action.payload.message 23 | }); 24 | } 25 | 26 | // User 27 | export const fetchAccountsSuccessReducer = createSuccessReducer("accounts"); 28 | 29 | // Network 30 | export const fetchNetworkSuccessReducer = createSuccessReducer("network"); 31 | export const fetchBlockNumberSuccessReducer = createSuccessReducer("currentBlock"); 32 | 33 | // FundingHub 34 | export const fetchProjectsSuccessReducer = createSuccessReducer("projects"); 35 | 36 | // Project 37 | export const fetchProjectSuccessReducer = createSuccessReducer("project"); 38 | export const fetchContributionsSuccessReducer = createSuccessReducer("contributions"); 39 | -------------------------------------------------------------------------------- /src/actions/fundingHubActions.js: -------------------------------------------------------------------------------- 1 | import * as Web3Api from '../api/web3Api'; 2 | 3 | export const fetchProjectsRequest = "@@fundingHub/FETCH_PROJECTS_REQUEST"; 4 | export const fetchProjectsSuccess = "@@fundingHub/FETCH_PROJECTS_SUCCESS"; 5 | export const fetchProjectsFailure = "@@fundingHub/FETCH_PROJECTS_FAILURE"; 6 | export const createProjectTransaction = "@@fundingHub/CREATE_PROJECT_TRANSACTION"; 7 | export const createProjectSuccess = "@@fundingHub/CREATE_PROJECT_SUCCESS"; 8 | export const createProjectFailure = "@@fundingHub/CREATE_PROJECT_FAILURE"; 9 | 10 | export function fetchProjects() { 11 | console.log("fundingHubActions.fetchProjects()"); 12 | return { 13 | types: [ 14 | fetchProjectsRequest, 15 | fetchProjectsSuccess, 16 | fetchProjectsFailure, 17 | ], 18 | callApi: () => Web3Api.getProjects(), 19 | payload: {} 20 | }; 21 | }; 22 | 23 | export function createProject(projectParams, creatorAddress) { 24 | console.log("fundingHubActions.createProject() projectParams: ", projectParams); 25 | return { 26 | types: [ 27 | createProjectTransaction, 28 | createProjectSuccess, 29 | createProjectFailure, 30 | ], 31 | callApi: () => Web3Api.createProject(projectParams, creatorAddress), 32 | payload: {} 33 | }; 34 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | Crowdfund Dapp 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/reducers/fundingHubReducer.js: -------------------------------------------------------------------------------- 1 | import {handleActions} from 'redux-actions'; 2 | 3 | import { 4 | fetchProjectsRequest, 5 | fetchProjectsSuccess, 6 | fetchProjectsFailure, 7 | createProjectTransaction, 8 | createProjectSuccess, 9 | createProjectFailure, 10 | } from '../actions/fundingHubActions'; 11 | 12 | import { 13 | requestReducer, 14 | fetchProjectsSuccessReducer, 15 | failureReducer, 16 | } from './reducerUtil'; 17 | 18 | const initialState = { 19 | isFetching: false, 20 | projects: [], 21 | } 22 | 23 | function fetchProjectsSuccessReducerFunction(state, action) { 24 | console.log("fetchProjectsSuccessReducerFunction", action); 25 | return Object.assign({}, state, { 26 | isFetching: false, 27 | fetchComplete: true, 28 | projects: action.payload 29 | }); 30 | } 31 | 32 | const fetchProjectSuc = (state, action) => { 33 | console.log("fetchProjectsSuc", action); 34 | return Object.assign({}, state, { 35 | projects: action.payload, 36 | }); 37 | }; 38 | 39 | function fetchProjectsFailureReducer(state, action) { 40 | console.log("fetchProjectsFailureReducer: ", action); 41 | return state; 42 | } 43 | 44 | export const fundingHubReducer = handleActions({ 45 | [fetchProjectsRequest]: requestReducer, 46 | [fetchProjectsSuccess]: fetchProjectsSuccessReducer, 47 | [fetchProjectsFailure]: fetchProjectsFailureReducer, 48 | }, initialState); -------------------------------------------------------------------------------- /src/components/contributionList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from 'semantic-ui-react'; 3 | import { getEtherscanLink } from '../utils/web3Utils'; 4 | 5 | class ContributionList extends Component { 6 | render() { 7 | const { contributions, isLoading } = this.props; 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | Contributor 15 | Amount 16 | 17 | 18 | 19 | {contributions.map((contribution, index) => ( 20 | 21 | 22 | {contribution.contributor} 23 | 24 | 25 | {contribution.amount} ETH 26 | 27 | 28 | ))} 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | ContributionList.PropTypes = { 36 | contributions: React.PropTypes.array.isRequired, 37 | } 38 | 39 | export default ContributionList; -------------------------------------------------------------------------------- /src/utils/web3Utils.js: -------------------------------------------------------------------------------- 1 | export function getExtendedWeb3Provider(web3Provider) { 2 | // Found here https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6 3 | web3Provider.eth.getTransactionReceiptMined = function (txnHash, interval) { 4 | var transactionReceiptAsync; 5 | interval = interval ? interval : 500; 6 | transactionReceiptAsync = function(txnHash, resolve, reject) { 7 | try { 8 | var receipt = web3Provider.eth.getTransactionReceipt(txnHash); 9 | if (receipt == null) { 10 | setTimeout(function () { 11 | transactionReceiptAsync(txnHash, resolve, reject); 12 | }, interval); 13 | } else { 14 | resolve(receipt); 15 | } 16 | } catch(e) { 17 | reject(e); 18 | } 19 | }; 20 | 21 | if (Array.isArray(txnHash)) { 22 | var promises = []; 23 | txnHash.forEach(function (oneTxHash) { 24 | promises.push(web3Provider.eth.getTransactionReceiptMined(oneTxHash, interval)); 25 | }); 26 | return Promise.all(promises); 27 | } else { 28 | return new Promise(function (resolve, reject) { 29 | transactionReceiptAsync(txnHash, resolve, reject); 30 | }); 31 | } 32 | }; 33 | 34 | return web3Provider; 35 | } 36 | 37 | export function getEtherscanLink(address) { 38 | return `https://testnet.etherscan.io/address/${address}`; 39 | } -------------------------------------------------------------------------------- /src/components/navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Menu, Dropdown, Container } from 'semantic-ui-react'; 4 | import { getFormattedUserAccount } from '../utils/textUtils'; 5 | 6 | class Navigation extends Component { 7 | 8 | render() { 9 | const { 10 | user: { 11 | accounts, 12 | selectedAccount 13 | } 14 | } = this.props; 15 | 16 | let selectedDropdown = ""; 17 | if (accounts.length > 0) { 18 | selectedDropdown = getFormattedUserAccount(accounts[selectedAccount]); 19 | } 20 | 21 | return ( 22 | 26 | 27 | 28 | 29 | 30 | Crowdfund Dapp 31 | 32 | 33 | 34 | 35 | 36 | {accounts.map((account, index) => 37 | this.props.onHandleSelectAccount(index)}> 40 | {getFormattedUserAccount(account)} 41 | 42 | )} 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | export default Navigation; -------------------------------------------------------------------------------- /src/components/projectCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Card, Button, Icon, Loader, Progress, Breadcrumb, Grid, Container } from 'semantic-ui-react'; 3 | import { getFormattedProgressPercentage } from '../utils/projectUtils'; 4 | 5 | import './projectCard.css'; 6 | 7 | var _this; 8 | 9 | class ProjectCard extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | _this = this; 14 | } 15 | 16 | handleContributeClicked =() => { 17 | this.props.onContributeClicked(); 18 | } 19 | 20 | render() { 21 | const { project, isLoading } = _this.props; 22 | return( 23 | 24 |
25 | 26 | 27 | 28 | {project.title} 29 | 30 | 31 | {project.totalFunding + " / " + project.goal + " ETH"} 32 | 33 | 34 | {getFormattedProgressPercentage(project.totalFunding, project.goal) + "%"} 35 | 36 | 37 | 38 |
39 |
40 | ) 41 | } 42 | } 43 | 44 | ProjectCard.PropTypes = { 45 | project: React.PropTypes.object.isRequired, 46 | isLoading: React.PropTypes.bool.isRequired, 47 | onContributeClicked: React.PropTypes.func.isRequired, 48 | } 49 | 50 | export default ProjectCard; -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 23 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 24 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 25 | 26 | var nodePaths = (process.env.NODE_PATH || '') 27 | .split(process.platform === 'win32' ? ';' : ':') 28 | .filter(Boolean) 29 | .filter(folder => !path.isAbsolute(folder)) 30 | .map(resolveApp); 31 | 32 | // config after eject: we're in ./config/ 33 | module.exports = { 34 | // Changed from build to build_webpack so smart contract compilations are not overwritten. 35 | appBuild: resolveApp('build_webpack'), 36 | appPublic: resolveApp('public'), 37 | appHtml: resolveApp('public/index.html'), 38 | appIndexJs: resolveApp('src/index.js'), 39 | appPackageJson: resolveApp('package.json'), 40 | appSrc: resolveApp('src'), 41 | yarnLockFile: resolveApp('yarn.lock'), 42 | testsSetup: resolveApp('src/setupTests.js'), 43 | appNodeModules: resolveApp('node_modules'), 44 | ownNodeModules: resolveApp('node_modules'), 45 | nodePaths: nodePaths 46 | }; 47 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes 1; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | # CUSTOM CONFIG 30 | # This is how we host our static React site. 31 | server { 32 | 33 | # Listen on this port. This would be 80 or 443 on a prod server. Adjust this 34 | # to suit your own needs. 35 | listen 80; 36 | 37 | # Server base URL goes here if applicable 38 | #server_name trustar.co; 39 | 40 | location / { 41 | 42 | # Enable gzip. NOTE: text/html files are always gzipped when enabled 43 | gzip on; 44 | gzip_min_length 1000; 45 | gzip_types text/plain text/css application/javascript application/json image/x-icon; 46 | 47 | # The location of the static files to server 48 | root /var/www/html; 49 | 50 | # Remove trailing slashes. /about/ -> /about 51 | # This is important because of how static files are generated. 52 | rewrite ^/(.*)/$ /$1 permanent; 53 | 54 | # If migrating from a dynamic site you may want to redirect requests to a 55 | # certain path to a different server. This example redirects all traffic 56 | # to /blog to blog.example.com 57 | #rewrite ^/blog(.*)$ $scheme://blog.example.com$1 redirect; 58 | 59 | # Use 404.html as the error page 60 | error_page 404 /404.html; 61 | 62 | # If a matching file can't be found, handle this request as a 404, which 63 | # will return the 404 page because of the above directive 64 | try_files $uri $uri.html $uri/index.html =404; 65 | 66 | } 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /src/components/projectList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Item, Label, Loader, Progress } from 'semantic-ui-react'; 3 | import {getFormattedProgressPercentage, getProjectStatus} from '../utils/projectUtils'; 4 | 5 | class ProjectList extends Component { 6 | 7 | render() { 8 | const {projects, isLoading, currentBlock} = this.props; 9 | return ( 10 |
11 | 12 | 13 | {projects.map((project, index) => 14 | this.props.onProjectClicked(project.address)}> 15 | 16 | 17 | {project.title} 18 | 19 | {project.totalFunding + " / " + project.goal + " ETH"} 20 | 21 | 22 | 23 | {getFormattedProgressPercentage(project.totalFunding, project.goal) + "%"} 24 | 25 | 26 | 27 | 30 | 31 | 32 | )} 33 | 34 |
35 | ) 36 | } 37 | } 38 | 39 | ProjectList.PropTypes = { 40 | currentBlock: React.PropTypes.number.isRequired, 41 | projects: React.PropTypes.array.isRequired, 42 | isLoading: React.PropTypes.bool.isRequired, 43 | onProjectClicked: React.PropTypes.func.isRequired, 44 | } 45 | 46 | export default ProjectList; -------------------------------------------------------------------------------- /src/components/projectDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { getEtherscanLink } from '../utils/web3Utils'; 3 | 4 | import { Segment, Message, Header, Item } from 'semantic-ui-react'; 5 | 6 | class ProjectDetails extends Component { 7 | render() { 8 | const { project, balance, currentBlock } = this.props; 9 | return ( 10 |
11 | 14 | 15 |
Contract address:
16 | {project.address} 17 |
18 | 19 |
Raised:
20 | {project.totalFunding} ETH 21 |
22 | 23 |
Remaining blocks:
24 | {project.deadline - currentBlock} 25 |
26 | 27 |
Contributors:
28 | {project.contributorsCount} 29 |
30 | 31 | 32 | 33 |
Contributions:
34 |
35 | 36 | {project.contributionsCount} 37 | 38 |
39 |
40 | 41 |
Creator:
42 | {project.creator} 43 |
44 | 45 |
Balance
46 | { balance } ETH 47 |
48 |
49 | ) 50 | } 51 | } 52 | 53 | ProjectDetails.PropTypes = { 54 | project: React.PropTypes.object.isRequired, 55 | balance: React.PropTypes.number.isRequired, 56 | currentBlock: React.PropTypes.number.isRequired, 57 | } 58 | 59 | export default ProjectDetails; -------------------------------------------------------------------------------- /src/reducers/projectReducer.js: -------------------------------------------------------------------------------- 1 | import {handleActions} from 'redux-actions'; 2 | 3 | import { 4 | fetchProjectRequest, 5 | fetchProjectSuccess, 6 | fetchProjectFailure, 7 | contributeProjectRequest, 8 | contributeProjectSuccess, 9 | contributeProjectFailure, 10 | fetchProjectBalanceRequest, 11 | fetchProjectBalanceSuccess, 12 | fetchProjectBalanceFailure, 13 | fetchContributionsRequest, 14 | fetchContributionsSuccess, 15 | fetchContributionsFailure 16 | } from '../actions/projectActions'; 17 | 18 | import { 19 | requestReducer, 20 | fetchProjectSuccessReducer, 21 | fetchContributionsSuccessReducer, 22 | failureReducer, 23 | } from './reducerUtil'; 24 | 25 | const initialState = { 26 | isFetching: false, 27 | project: { 28 | title: "-", 29 | goal: 0, 30 | deadline: 0, 31 | creator: "-", 32 | totalFunding: 0, 33 | contributionCount: 0, 34 | contributorsCount: 0, 35 | fundingHub: "-", 36 | address: "-", 37 | }, 38 | contributionSuccessful: true, 39 | balance: 0, 40 | contributions: [], 41 | } 42 | 43 | function fetchProjectBalanceSuccessReducer(state, action) { 44 | console.log("state: ", state); 45 | return Object.assign({}, state, { 46 | isFetching: false, 47 | fetchComplete: true, 48 | balance: action.payload 49 | }); 50 | }; 51 | 52 | function contributeProjectSuccessReducer(state, action) { 53 | console.log("action: ", action); 54 | return Object.assign({}, state, { 55 | isFetching: false, 56 | fetchComplete: true, 57 | contributionSuccessful: true 58 | }); 59 | } 60 | 61 | export const projectReducer = handleActions({ 62 | [fetchProjectRequest]: requestReducer, 63 | [fetchProjectSuccess]: fetchProjectSuccessReducer, 64 | [fetchProjectFailure]: failureReducer, 65 | [fetchProjectBalanceRequest]: requestReducer, 66 | [fetchProjectBalanceSuccess]: fetchProjectBalanceSuccessReducer, 67 | [fetchProjectBalanceFailure]: failureReducer, 68 | [contributeProjectRequest]: requestReducer, 69 | [contributeProjectSuccess]: contributeProjectSuccessReducer, 70 | [contributeProjectFailure]: failureReducer, 71 | [fetchContributionsRequest]: requestReducer, 72 | [fetchContributionsSuccess]: fetchContributionsSuccessReducer, 73 | [fetchContributionsFailure]: failureReducer, 74 | }, initialState); -------------------------------------------------------------------------------- /src/components/contributeModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Modal, Form, Button } from 'semantic-ui-react'; 3 | import { toWei } from '../api/web3Api'; 4 | 5 | const initialState = { 6 | amountInEth: 0 7 | } 8 | 9 | class ContributeModal extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = initialState; 15 | } 16 | 17 | handleChange = (e, { name, value }) => this.setState({ [name]: value }) 18 | 19 | handleClose = () => { 20 | this.props.onCloseModal(); 21 | this.setState(initialState); 22 | } 23 | 24 | handleContribute = () => { 25 | console.log(this.state.amountInEth); 26 | this.props.onHandleContribute(this.state.amountInEth); 27 | this.setState(initialState); 28 | } 29 | 30 | render() { 31 | const { isDisplayed } = this.props; 32 | return ( 33 |
34 | 38 | Contribute to this project 39 | 40 | 41 |
42 | 48 | 49 |
50 |
51 | 52 | 53 | 54 | 55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | ContributeModal.PropTypes = { 62 | isDisplayed: React.PropTypes.bool.isRequired, 63 | gasCost: React.PropTypes.number.isRequired, 64 | onCloseModal: React.PropTypes.func.isRequired, 65 | onHandleContribute: React.PropTypes.func.isRequired, 66 | } 67 | 68 | export default ContributeModal; -------------------------------------------------------------------------------- /src/actions/projectActions.js: -------------------------------------------------------------------------------- 1 | import * as Web3Api from '../api/web3Api'; 2 | 3 | export const fetchProjectRequest = "@@project/FETCH_PROJECT_REQUEST"; 4 | export const fetchProjectSuccess = "@@project/FETCH_PROJECT_SUCCESS"; 5 | export const fetchProjectFailure = "@@project/FETCH_PROJECT_FAILURE"; 6 | 7 | export const fetchProjectBalanceRequest = "@@project/FETCH_PROJECT_BALANCE_REQUEST"; 8 | export const fetchProjectBalanceSuccess = "@@project/FETCH_PROJECT_BALANCE_SUCCESS"; 9 | export const fetchProjectBalanceFailure = "@@project/FETCH_PROJECT_BALANCE_FAILURE"; 10 | 11 | export const contributeProjectRequest = "@@project/CONTRIBUTE_PROJECT_REQUEST"; 12 | export const contributeProjectSuccess = "@@project/CONTRIBUTE_PROJECT_SUCCESS"; 13 | export const contributeProjectFailure = "@@project/CONTRIBUTE_PROJECT_FAILURE"; 14 | 15 | export const fetchContributionsRequest = "@@/project/FETCH_CONTRIBUTION_REQUEST"; 16 | export const fetchContributionsSuccess = "@@/project/FETCH_CONTRIBUTION_SUCCESS"; 17 | export const fetchContributionsFailure = "@@/project/FETCH_CONTRIBUTION_FAILURE"; 18 | 19 | export function fetchProject(contractAddress) { 20 | return { 21 | types: [ 22 | fetchProjectRequest, 23 | fetchProjectSuccess, 24 | fetchProjectFailure, 25 | ], 26 | callApi: () => Web3Api.getProjectDetails(contractAddress), 27 | payload: {} 28 | }; 29 | }; 30 | 31 | export function contribute(contractAddress, amount, userAddress) { 32 | return { 33 | types: [ 34 | contributeProjectRequest, 35 | contributeProjectSuccess, 36 | contributeProjectFailure, 37 | ], 38 | callApi: () => Web3Api.contribute(contractAddress, amount, userAddress), 39 | payload: {} 40 | }; 41 | } 42 | 43 | export function fetchProjectBalance(contractAddress) { 44 | return { 45 | types: [ 46 | fetchProjectBalanceRequest, 47 | fetchProjectBalanceSuccess, 48 | fetchProjectBalanceFailure, 49 | ], 50 | callApi: () => Web3Api.getAddressBalance(contractAddress), 51 | payload: {} 52 | }; 53 | }; 54 | 55 | export function fetchContributions(contractAddress) { 56 | return { 57 | types: [ 58 | fetchContributionsRequest, 59 | fetchContributionsSuccess, 60 | fetchContributionsFailure, 61 | ], 62 | callApi: () => Web3Api.getProjectContributions(contractAddress), 63 | payload: {} 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /test/metacoin.js: -------------------------------------------------------------------------------- 1 | var MetaCoin = artifacts.require("./MetaCoin.sol"); 2 | 3 | contract('MetaCoin', function(accounts) { 4 | it("should put 10000 MetaCoin in the first account", function() { 5 | return MetaCoin.deployed().then(function(instance) { 6 | return instance.getBalance.call(accounts[0]); 7 | }).then(function(balance) { 8 | assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); 9 | }); 10 | }); 11 | it("should call a function that depends on a linked library", function() { 12 | var meta; 13 | var metaCoinBalance; 14 | var metaCoinEthBalance; 15 | 16 | return MetaCoin.deployed().then(function(instance) { 17 | meta = instance; 18 | return meta.getBalance.call(accounts[0]); 19 | }).then(function(outCoinBalance) { 20 | metaCoinBalance = outCoinBalance.toNumber(); 21 | return meta.getBalanceInEth.call(accounts[0]); 22 | }).then(function(outCoinBalanceEth) { 23 | metaCoinEthBalance = outCoinBalanceEth.toNumber(); 24 | }).then(function() { 25 | assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpeced function, linkage may be broken"); 26 | }); 27 | }); 28 | it("should send coin correctly", function() { 29 | var meta; 30 | 31 | // Get initial balances of first and second account. 32 | var account_one = accounts[0]; 33 | var account_two = accounts[1]; 34 | 35 | var account_one_starting_balance; 36 | var account_two_starting_balance; 37 | var account_one_ending_balance; 38 | var account_two_ending_balance; 39 | 40 | var amount = 10; 41 | 42 | return MetaCoin.deployed().then(function(instance) { 43 | meta = instance; 44 | return meta.getBalance.call(account_one); 45 | }).then(function(balance) { 46 | account_one_starting_balance = balance.toNumber(); 47 | return meta.getBalance.call(account_two); 48 | }).then(function(balance) { 49 | account_two_starting_balance = balance.toNumber(); 50 | return meta.sendCoin(account_two, amount, {from: account_one}); 51 | }).then(function() { 52 | return meta.getBalance.call(account_one); 53 | }).then(function(balance) { 54 | account_one_ending_balance = balance.toNumber(); 55 | return meta.getBalance.call(account_two); 56 | }).then(function(balance) { 57 | account_two_ending_balance = balance.toNumber(); 58 | 59 | assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender"); 60 | assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver"); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /contracts/FundingHub.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | import "./Project.sol"; 3 | 4 | contract FundingHub { 5 | 6 | address public owner; 7 | uint public numOfProjects; 8 | 9 | mapping (uint => address) public projects; 10 | 11 | event LogProjectCreated(uint id, string title, address addr, address creator); 12 | event LogContributionSent(address projectAddress, address contributor, uint amount); 13 | 14 | event LogFailure(string message); 15 | 16 | modifier onlyOwner { 17 | if (owner != msg.sender) throw; 18 | _; 19 | } 20 | 21 | function FundingHub() { 22 | owner = msg.sender; 23 | numOfProjects = 0; 24 | } 25 | 26 | /** 27 | * Create a new Project contract 28 | * [0] -> new Project contract address 29 | */ 30 | function createProject(uint _fundingGoal, uint _deadline, string _title) payable returns (Project projectAddress) { 31 | 32 | if (_fundingGoal <= 0) { 33 | LogFailure("Project funding goal must be greater than 0"); 34 | throw; 35 | } 36 | 37 | if (block.number >= _deadline) { 38 | LogFailure("Project deadline must be greater than the current block"); 39 | throw; 40 | } 41 | 42 | Project p = new Project(_fundingGoal, _deadline, _title, msg.sender); 43 | projects[numOfProjects] = p; 44 | LogProjectCreated(numOfProjects, _title, p, msg.sender); 45 | numOfProjects++; 46 | return p; 47 | } 48 | 49 | /** 50 | * Allow senders to contribute to a Project by it's address. Calls the fund() function in the Project 51 | * contract and passes on all value attached to this function call 52 | * [0] -> contribution was sent 53 | */ 54 | function contribute(address _projectAddress) payable returns (bool successful) { 55 | 56 | // Check amount sent is greater than 0 57 | if (msg.value <= 0) { 58 | LogFailure("Contributions must be greater than 0 wei"); 59 | throw; 60 | } 61 | 62 | Project deployedProject = Project(_projectAddress); 63 | 64 | // Check that there is actually a Project contract at that address 65 | if (deployedProject.fundingHub() == address(0)) { 66 | LogFailure("Project contract not found at address"); 67 | throw; 68 | } 69 | 70 | // Check that fund call was successful 71 | if (deployedProject.fund.value(msg.value)(msg.sender)) { 72 | LogContributionSent(_projectAddress, msg.sender, msg.value); 73 | return true; 74 | } else { 75 | LogFailure("Contribution did not send successfully"); 76 | return false; 77 | } 78 | } 79 | 80 | function kill() public onlyOwner { 81 | selfdestruct(owner); 82 | } 83 | 84 | /** 85 | * Don't allow Ether to be sent blindly to this contract 86 | */ 87 | function() { 88 | throw; 89 | } 90 | } -------------------------------------------------------------------------------- /src/containers/appContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import { Container, Message, Grid } from 'semantic-ui-react'; 4 | import Navigation from '../components/navigation'; 5 | 6 | import { 7 | fetchAccountsAndBalances, 8 | setSelectedAccount, 9 | } from '../actions/userActions'; 10 | 11 | import { 12 | fetchNetwork, 13 | fetchBlockNumber, 14 | } from '../actions/networkActions'; 15 | 16 | import './appContainer.css'; 17 | 18 | class AppContainer extends Component { 19 | 20 | componentDidMount() { 21 | const {dispatch} = this.props; 22 | dispatch(fetchNetwork()); 23 | dispatch(fetchBlockNumber()); 24 | dispatch(fetchAccountsAndBalances()); 25 | } 26 | 27 | handleSelectAccount = (accountIndex) => { 28 | const {dispatch} = this.props; 29 | dispatch(setSelectedAccount(accountIndex)); 30 | } 31 | 32 | getNetworkStatusAlert(networkId, currentBlock) { 33 | let networkDisplayName = "network"; 34 | 35 | switch (networkId) { 36 | case "1": 37 | networkDisplayName = "Mainnet"; 38 | break; 39 | case "2": 40 | networkDisplayName = "Morden"; 41 | break; 42 | case "3": 43 | networkDisplayName = "Ropsten"; 44 | break; 45 | case "42": 46 | networkDisplayName = "Kovan"; 47 | break; 48 | default: 49 | networkDisplayName = "TestRPC"; 50 | break; 51 | } 52 | 53 | let message = `Currently on ${networkDisplayName} (${networkId}), the current block is ${currentBlock}.`; 54 | 55 | return ( 56 | 59 | ) 60 | } 61 | 62 | render() { 63 | const { 64 | network: { 65 | network, 66 | currentBlock 67 | } 68 | } = this.props; 69 | 70 | let netId = ""; 71 | let curBlockNumber = 0; 72 | 73 | if (network !== "") { 74 | netId = network; 75 | } 76 | 77 | if (currentBlock !== 0) { 78 | curBlockNumber = currentBlock; 79 | } 80 | 81 | let content = ( 82 |
83 | 86 | {this.getNetworkStatusAlert(netId, curBlockNumber)} 87 | 88 | {this.props.children} 89 | 90 |
91 | ); 92 | 93 | return ( 94 | 95 | {content} 96 | 97 | ); 98 | } 99 | } 100 | 101 | function mapStateToProps(state) { 102 | return { 103 | user: state.user, 104 | network: state.network, 105 | }; 106 | } 107 | 108 | export default connect(mapStateToProps)(AppContainer); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truffle-box-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.5.1", 7 | "babel-core": "6.17.0", 8 | "babel-eslint": "7.1.1", 9 | "babel-jest": "17.0.2", 10 | "babel-loader": "6.2.7", 11 | "babel-preset-react-app": "^2.0.1", 12 | "case-sensitive-paths-webpack-plugin": "1.1.4", 13 | "chalk": "1.1.3", 14 | "connect-history-api-fallback": "1.3.0", 15 | "cross-spawn": "4.0.2", 16 | "css-loader": "0.26.0", 17 | "detect-port": "1.0.1", 18 | "dotenv": "2.0.0", 19 | "eslint": "3.8.1", 20 | "eslint-config-react-app": "^0.5.0", 21 | "eslint-loader": "1.6.0", 22 | "eslint-plugin-flowtype": "2.21.0", 23 | "eslint-plugin-import": "2.0.1", 24 | "eslint-plugin-jsx-a11y": "2.2.3", 25 | "eslint-plugin-react": "6.4.1", 26 | "extract-text-webpack-plugin": "1.0.1", 27 | "file-loader": "0.9.0", 28 | "filesize": "3.3.0", 29 | "fs-extra": "0.30.0", 30 | "gzip-size": "3.0.0", 31 | "html-webpack-plugin": "2.24.0", 32 | "http-proxy-middleware": "0.17.2", 33 | "jest": "17.0.2", 34 | "json-loader": "0.5.4", 35 | "object-assign": "4.1.0", 36 | "path-exists": "2.1.0", 37 | "postcss-loader": "1.0.0", 38 | "promise": "7.1.1", 39 | "react-dev-utils": "^0.4.2", 40 | "recursive-readdir": "2.1.0", 41 | "redux-devtools": "^3.3.2", 42 | "redux-devtools-inspector": "^0.11.3", 43 | "strip-ansi": "3.0.1", 44 | "style-loader": "0.13.1", 45 | "truffle-contract": "^1.1.8", 46 | "truffle-solidity-loader": "0.0.8", 47 | "url-loader": "0.5.7", 48 | "webpack": "1.14.0", 49 | "webpack-dev-server": "1.16.2", 50 | "webpack-manifest-plugin": "1.1.0", 51 | "whatwg-fetch": "1.0.0" 52 | }, 53 | "dependencies": { 54 | "dotenv": "^2.0.0", 55 | "react": "^15.3.1", 56 | "react-dom": "^15.3.1", 57 | "react-redux": "^4.4.6", 58 | "react-router": "^3.0.2", 59 | "react-router-redux": "^4.0.8", 60 | "redux": "^3.6.0", 61 | "redux-actions": "^2.0.2", 62 | "redux-devtools-dock-monitor": "^1.1.1", 63 | "redux-thunk": "^2.2.0", 64 | "semantic-ui-react": "^0.68.1" 65 | }, 66 | "scripts": { 67 | "start": "node scripts/start.js", 68 | "build": "node scripts/build.js", 69 | "test": "node scripts/test.js --env=jsdom" 70 | }, 71 | "jest": { 72 | "collectCoverageFrom": [ 73 | "src/**/*.{js,jsx}" 74 | ], 75 | "setupFiles": [ 76 | "\\config\\polyfills.js" 77 | ], 78 | "testPathIgnorePatterns": [ 79 | "[/\\\\](build|docs|node_modules)[/\\\\]" 80 | ], 81 | "testEnvironment": "node", 82 | "testURL": "http://localhost", 83 | "transform": { 84 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 85 | "^.+\\.css$": "\\config\\jest\\cssTransform.js", 86 | "^(?!.*\\.(js|jsx|css|json)$)": "\\config\\jest\\fileTransform.js" 87 | }, 88 | "transformIgnorePatterns": [ 89 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 90 | ], 91 | "moduleNameMapper": { 92 | "^react-native$": "react-native-web" 93 | } 94 | }, 95 | "babel": { 96 | "presets": [ 97 | "react-app" 98 | ] 99 | }, 100 | "eslintConfig": { 101 | "extends": "react-app" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/createProjectModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Modal, Form, Button, Select } from 'semantic-ui-react'; 3 | 4 | const BLOCKS_PER_DAY = 5082; 5 | const BLOCKS_PER_WEEK = 38117; 6 | const BLOCKS_PER_MONTH = 157553; 7 | 8 | const initialState = { 9 | projectName: "", 10 | projectGoalInEth: 0, 11 | projectDeadline: BLOCKS_PER_DAY 12 | } 13 | 14 | class CreateProjectModal extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = initialState; 19 | } 20 | 21 | handleChange = (e, { name, value }) => this.setState({ [name]: value }) 22 | 23 | handleClose = () => { 24 | this.props.onCloseModal(); 25 | this.setState(initialState); 26 | } 27 | 28 | handleCreate = () => { 29 | this.props.onHandleProjectCreate(this.state); 30 | this.setState(initialState); 31 | } 32 | 33 | render () { 34 | const {isDisplayed, gasCost} = this.props; 35 | 36 | return ( 37 |
38 | 42 | Create a new crowdfunding project 43 | 44 | 45 |
46 | 52 | 58 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | 75 |
76 |
77 | ) 78 | } 79 | } 80 | 81 | CreateProjectModal.PropTypes = { 82 | isDisplayed: React.PropTypes.bool.isRequired, 83 | gasCost: React.PropTypes.number.isRequired, 84 | currentBlock: React.PropTypes.number.isRequired, 85 | onCloseModal: React.PropTypes.func.isRequired, 86 | onHandleProjectCreate: React.PropTypes.func.isRequired, 87 | } 88 | 89 | export default CreateProjectModal; -------------------------------------------------------------------------------- /src/containers/homeContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import { Container, Header, Divider, Button, Message } from 'semantic-ui-react'; 4 | import { push } from 'react-router-redux'; 5 | 6 | import CreateProjectModal from '../components/createProjectModal'; 7 | import ProjectList from '../components/projectList'; 8 | import {createProject, fetchProjects} from '../actions/fundingHubActions'; 9 | 10 | 11 | var _this; 12 | 13 | class HomeContainer extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | _this = this; 18 | 19 | this.state = { 20 | showCreateProjectModal: false 21 | } 22 | } 23 | 24 | componentDidMount() { 25 | const {dispatch} = _this.props; 26 | dispatch(fetchProjects()); 27 | } 28 | 29 | toggleModalDisplayed() { 30 | _this.setState({ 31 | showCreateProjectModal: !_this.state.showCreateProjectModal 32 | }); 33 | } 34 | 35 | handleCreateProjectClicked() { 36 | _this.toggleModalDisplayed(); 37 | } 38 | 39 | handleCreateProject(project) { 40 | const {dispatch, user} = _this.props; 41 | 42 | _this.toggleModalDisplayed(); 43 | 44 | let selectedUserAddress = user.accounts[user.selectedAccount].address; 45 | 46 | if (!!selectedUserAddress) { 47 | dispatch(createProject(project, selectedUserAddress)) 48 | .then(() => { 49 | dispatch(fetchProjects()); 50 | }); 51 | } 52 | } 53 | 54 | handleProjectClicked(projectAddress) { 55 | const { dispatch } = _this.props; 56 | dispatch(push(`/project/${projectAddress}`)); 57 | } 58 | 59 | getProjectsMessage(fundingHub) { 60 | if (fundingHub.fetchComplete && fundingHub.projects.length === 0) { 61 | return ( 62 | 63 | No projects found 64 |

Start a crowdfunding project by clicking the button above

65 |
66 | ); 67 | } else { 68 | return null; 69 | } 70 | } 71 | 72 | render() { 73 | return ( 74 | 75 |
Explore projects
76 |

Crowdfund Dapp is a decentralized crowdfunding platform built on Ethereum. This site is intended to demonstrate a full featured dapp using the latest Ethereum and web development frameworks including Webpack, React, Redux, Semantic-ui, Solidity, Web3, and Truffle. Feel free to create projects and interact with it. All source code is available here and is based off truffle-box

77 | 82 | 83 | 88 | {this.getProjectsMessage(this.props.fundingHub)} 89 | 95 |
96 | ); 97 | } 98 | } 99 | 100 | function mapStateToProps(state) { 101 | return { 102 | user: state.user, 103 | network: state.network, 104 | fundingHub: state.fundingHub, 105 | } 106 | } 107 | 108 | export default connect(mapStateToProps)(HomeContainer); -------------------------------------------------------------------------------- /src/containers/projectContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import { Card, Grid } from 'semantic-ui-react'; 4 | import ProjectCard from '../components/projectCard'; 5 | import { 6 | fetchProject, 7 | contribute, 8 | fetchProjectBalance, 9 | fetchContributions 10 | } from '../actions/projectActions'; 11 | import { fetchAccountsAndBalances } from '../actions/userActions'; 12 | import ContributionList from '../components/contributionList'; 13 | import ProjectDetails from '../components/projectDetails'; 14 | import ContributeModal from '../components/contributeModal'; 15 | 16 | var _this; 17 | 18 | class ProjectContainer extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | _this = this; 23 | 24 | this.state = { 25 | showContributeModal: false 26 | } 27 | 28 | } 29 | 30 | componentDidMount() { 31 | const { dispatch, params } = _this.props; 32 | dispatch(fetchProject(params.address)); 33 | dispatch(fetchContributions(params.address)); 34 | dispatch(fetchProjectBalance(params.address)); 35 | } 36 | 37 | 38 | toggleModalDisplayed() { 39 | _this.setState({ 40 | showContributeModal: !_this.state.showContributeModal 41 | }); 42 | } 43 | 44 | handleContributeClicked() { 45 | _this.toggleModalDisplayed(); 46 | } 47 | 48 | handleContribute(amount) { 49 | const {dispatch, user, project} = _this.props; 50 | 51 | _this.toggleModalDisplayed(); 52 | 53 | let selectedUserAddress = user.accounts[user.selectedAccount].address; 54 | 55 | if (!!selectedUserAddress) { 56 | dispatch(contribute(project.project.address, amount, selectedUserAddress)) 57 | .then(() => { 58 | dispatch(fetchProject(project.project.address)); 59 | dispatch(fetchContributions(project.project.address)) 60 | dispatch(fetchAccountsAndBalances()); 61 | dispatch(fetchProjectBalance(project.project.address)); 62 | }); 63 | } 64 | } 65 | 66 | render() { 67 | const { 68 | project, 69 | network: { 70 | network, 71 | currentBlock 72 | } 73 | } = this.props; 74 | 75 | let projectDetails = project.project; 76 | let contributions = project.contributions; 77 | 78 | return ( 79 | 80 | 81 | 82 | 86 | 87 | 88 | 92 | 93 | 98 | 99 | 100 | 102 | 103 | 104 | ) 105 | } 106 | } 107 | 108 | function mapStateToProps(state) { 109 | return { 110 | user: state.user, 111 | project: state.project, 112 | network: state.network, 113 | } 114 | } 115 | 116 | export default connect(mapStateToProps)(ProjectContainer); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Decentralized Crowdfunding App 2 | 3 | This is a simple crowdfunding dapp intended to show off what I've learned from the B9 Lab Ethereum course. The contracts are written in Solidity and the app is utilizing the Truffle framework. The frontend of the app is built with React and Webpack. 4 | 5 | [https://github.com/tyndallm/crowdfund-dapp](https://github.com/tyndallm/crowdfund-dapp) 6 | 7 | ### Contracts 8 | 9 | **FundingHub.sol** 10 | The first contract is the Funding Hub. This contract is responsible for creating and maintaining a list of all Project contracts. FundingHub also offers a contribute method which can be used to contribute directly to a Project. To demonstrate a potential business model use-case Projects have been locked to only allow receiving of funds from their managing Funding Hub. You can imagine a scenario in which the FundingHub takes a small fee for managing each project. 11 | 12 | **Project.sol** 13 | This contract contains all of the logic around how a crowdfunding project should operate. Projects are "locked" to their Funding Hub and can only receive funds sent thru the associated FundingHub contract address. 14 | 15 | There are three main functions: (fund, payout, and refund) 16 | 17 | *Fund* 18 | This is the function called when the FundingHub receives a contribution. If the contribution was sent after the deadline of the project passed, or the full amount has been reached, the function must return the value to the originator of the transaction. If the full funding amount has been reached, the function must call payout. 19 | **NOTE**: This is slightly different than the original instructions. I wanted to enforce the withdrawal pattern in the refund method as opposed to a group send. The withdrawal pattern is generally considered safer and avoids some of the pitfalls of call depth and out-of-gas issues, [see more here](https://solidity.readthedocs.io/en/develop/common-patterns.html#withdrawal-from-contracts). 20 | 21 | *Payout* 22 | If funding goal has been met, transfer fund to project creator. This function protects against re-entrancy and is only payable to the project creator. 23 | 24 | *Refund* 25 | If the deadline is passed and the goal was not reached, allow contributors to withdraw their contributions. 26 | **NOTE** This is slightly different that the final project requirements, see above for details. 27 | 28 | 29 | ### App 30 | The frontend app for this project is built on React and forks off of the [truffle-webpack-demo project](https://github.com/ConsenSys/truffle-webpack-demo) by Consensys. The cool thing about this is that it combines the latest in regular frontend javscript development with Ethereum. In order to manage the state of the dapp, Redux was chosen. 31 | 32 | One of my goals of this project was to see if there was a way I could abstract the asynchronous web3 and contract calls into a simple API that I could then integrate into a standard React+Redux Action/Reducer flow. This was achieved with the web3Api.js file. This approach works well with the asynchronous nature of interacting with the blockchain as things like contract properties, and account balances can seemlessly notify the app when they have updated and the UI will reflect those changes instantly. 33 | 34 | Here is an example of how this flow works: 35 | 36 | 0. Upon initial load the app dispatchs ```fetchProjectsAndDetails()``` 37 | 0. fetchProjectsAndDetails dispatchs the ```requestProjects``` Action and makes async request to web3Api's ```getProjects()``` function 38 | 0. When the ```getProjects()``` request resolves it returns the result to the ```fetchProjectAndDetails()``` function and dispatchs the ```receivedProjects``` Action which notifies the FundingHub Reducer that the state has changed. 39 | 0. When the app sees that the state for FundingHub has changed, the UI re-renders with the new project properties and the projects are displayed in a table on the page 40 | 41 | 42 | ### Screenshots 43 | ![Funding Hub screen](https://github.com/tyndallm/crowdfund-dapp/raw/master/public/funding_hub_screen.png) 44 | ![Create project screen](https://github.com/tyndallm/crowdfund-dapp/raw/master/public/create_project.png) 45 | ![Project screen](https://github.com/tyndallm/crowdfund-dapp/raw/master/public/project.png) 46 | 47 | 48 | ### Running 49 | 50 | The Web3 RPC location will be picked up from the `truffle.js` file. 51 | 52 | 0. Clone this repo 53 | 0. `npm install` 54 | 0. Make sure `testrpc` is running on its default port. Then: 55 | - `npm run start` - Starts the development server 56 | - `npm run build` - Generates a build 57 | - `truffle test` - Run the rest suite -------------------------------------------------------------------------------- /src/fonts/Oswald-300/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Oswald Project Authors (contact@sansoxygen.com) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/fonts/Oswald-regular/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Oswald Project Authors (contact@sansoxygen.com) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /contracts/Project.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Project { 4 | 5 | struct Properties { 6 | uint goal; 7 | uint deadline; 8 | string title; 9 | address creator; 10 | } 11 | 12 | struct Contribution { 13 | uint amount; 14 | address contributor; 15 | } 16 | 17 | address public fundingHub; 18 | 19 | mapping (address => uint) public contributors; 20 | mapping (uint => Contribution) public contributions; 21 | 22 | uint public totalFunding; 23 | uint public contributionsCount; 24 | uint public contributorsCount; 25 | 26 | Properties public properties; 27 | 28 | event LogContributionReceived(address projectAddress, address contributor, uint amount); 29 | event LogPayoutInitiated(address projectAddress, address owner, uint totalPayout); 30 | event LogRefundIssued(address projectAddress, address contributor, uint refundAmount); 31 | event LogFundingGoalReached(address projectAddress, uint totalFunding, uint totalContributions); 32 | event LogFundingFailed(address projectAddress, uint totalFunding, uint totalContributions); 33 | 34 | event LogFailure(string message); 35 | 36 | modifier onlyFundingHub { 37 | if (fundingHub != msg.sender) throw; 38 | _; 39 | } 40 | 41 | modifier onlyFunded { 42 | if (totalFunding < properties.goal) { 43 | throw; 44 | } 45 | _; 46 | } 47 | 48 | function Project(uint _fundingGoal, uint _deadline, string _title, address _creator) { 49 | 50 | // Check to see the funding goal is greater than 0 51 | if (_fundingGoal <= 0) { 52 | LogFailure("Project funding goal must be greater than 0"); 53 | throw; 54 | } 55 | 56 | // Check to see the deadline is in the future 57 | if (block.number >= _deadline) { 58 | LogFailure("Project deadline must be greater than the current block"); 59 | throw; 60 | } 61 | 62 | // Check to see that a creator (payout) address is valid 63 | if (_creator == 0) { 64 | LogFailure("Project must include a valid creator address"); 65 | throw; 66 | } 67 | 68 | fundingHub = msg.sender; 69 | 70 | // initialize properties struct 71 | properties = Properties({ 72 | goal: _fundingGoal, 73 | deadline: _deadline, 74 | title: _title, 75 | creator: _creator 76 | }); 77 | 78 | totalFunding = 0; 79 | contributionsCount = 0; 80 | contributorsCount = 0; 81 | } 82 | 83 | /** 84 | * Project values are indexed in return value: 85 | * [0] -> Project.properties.title 86 | * [1] -> Project.properties.goal 87 | * [2] -> Project.properties.deadline 88 | * [3] -> Project.properties.creator 89 | * [4] -> Project.totalFunding 90 | * [5] -> Project.contributionsCount 91 | * [6] -> Project.contributorsCount 92 | * [7] -> Project.fundingHub 93 | * [8] -> Project (address) 94 | */ 95 | function getProject() returns (string, uint, uint, address, uint, uint, uint, address, address) { 96 | return (properties.title, 97 | properties.goal, 98 | properties.deadline, 99 | properties.creator, 100 | totalFunding, 101 | contributionsCount, 102 | contributorsCount, 103 | fundingHub, 104 | address(this)); 105 | } 106 | 107 | /** 108 | * Retrieve indiviual contribution information 109 | * [0] -> Contribution.amount 110 | * [1] -> Contribution.contributor 111 | */ 112 | function getContribution(uint _id) returns (uint, address) { 113 | Contribution c = contributions[_id]; 114 | return (c.amount, c.contributor); 115 | } 116 | 117 | /** 118 | * This is the function called when the FundingHub receives a contribution. 119 | * If the contribution was sent after the deadline of the project passed, 120 | * or the full amount has been reached, the function must return the value 121 | * to the originator of the transaction. 122 | * If the full funding amount has been reached, the function must call payout. 123 | * [0] -> contribution was made 124 | */ 125 | function fund(address _contributor) payable returns (bool successful) { 126 | 127 | // Check amount is greater than 0 128 | if (msg.value <= 0) { 129 | LogFailure("Funding contributions must be greater than 0 wei"); 130 | throw; 131 | } 132 | 133 | // Check funding only comes thru fundingHub 134 | if (msg.sender != fundingHub) { 135 | LogFailure("Funding contributions can only be made through FundingHub contract"); 136 | throw; 137 | } 138 | 139 | // 1. Check that the project dealine has not passed 140 | if (block.number > properties.deadline) { 141 | LogFundingFailed(address(this), totalFunding, contributionsCount); 142 | if (!_contributor.send(msg.value)) { 143 | LogFailure("Project deadline has passed, problem returning contribution"); 144 | throw; 145 | } 146 | return false; 147 | } 148 | 149 | // 2. Check that funding goal has not already been met 150 | if (totalFunding >= properties.goal) { 151 | LogFundingGoalReached(address(this), totalFunding, contributionsCount); 152 | if (!_contributor.send(msg.value)) { 153 | LogFailure("Project deadline has passed, problem returning contribution"); 154 | throw; 155 | } 156 | payout(); 157 | return false; 158 | } 159 | 160 | // determine if this is a new contributor 161 | uint prevContributionBalance = contributors[_contributor]; 162 | 163 | // Add contribution to contributions map 164 | Contribution c = contributions[contributionsCount]; 165 | c.contributor = _contributor; 166 | c.amount = msg.value; 167 | 168 | // Update contributor's balance 169 | contributors[_contributor] += msg.value; 170 | 171 | totalFunding += msg.value; 172 | contributionsCount++; 173 | 174 | // Check if contributor is new and if so increase count 175 | if (prevContributionBalance == 0) { 176 | contributorsCount++; 177 | } 178 | 179 | LogContributionReceived(this, _contributor, msg.value); 180 | 181 | // Check again to see whether the last contribution met the fundingGoal 182 | if (totalFunding >= properties.goal) { 183 | LogFundingGoalReached(address(this), totalFunding, contributionsCount); 184 | payout(); 185 | } 186 | 187 | return true; 188 | } 189 | 190 | /** 191 | * If funding goal has been met, transfer fund to project creator 192 | * [0] -> payout was successful 193 | */ 194 | function payout() payable onlyFunded returns (bool successful) { 195 | uint amount = totalFunding; 196 | 197 | // prevent re-entrancy 198 | totalFunding = 0; 199 | 200 | if (properties.creator.send(amount)) { 201 | return true; 202 | } else { 203 | totalFunding = amount; 204 | return false; 205 | } 206 | 207 | return true; 208 | } 209 | 210 | /** 211 | * If the deadline is passed and the goal was not reached, allow contributors to withdraw their contributions. 212 | * This is slightly different that the final project requirements, see README for details 213 | * [0] -> refund was successful 214 | */ 215 | function refund() payable returns (bool successful) { 216 | 217 | // Check that the project dealine has passed 218 | if (block.number < properties.deadline) { 219 | LogFailure("Refund is only possible if project is past deadline"); 220 | throw; 221 | } 222 | 223 | // Check that funding goal has not already been met 224 | if (totalFunding >= properties.goal) { 225 | LogFailure("Refund is not possible if project has met goal"); 226 | throw; 227 | } 228 | 229 | uint amount = contributors[msg.sender]; 230 | 231 | //prevent re-entrancy attack 232 | contributors[msg.sender] = 0; 233 | 234 | if (msg.sender.send(amount)) { 235 | LogRefundIssued(address(this), msg.sender, amount); 236 | return true; 237 | } else { 238 | contributors[msg.sender] = amount; 239 | LogFailure("Refund did not send successfully"); 240 | return false; 241 | } 242 | return true; 243 | } 244 | 245 | function kill() public onlyFundingHub { 246 | selfdestruct(fundingHub); 247 | } 248 | 249 | /** 250 | * Don't allow Ether to be sent blindly to this contract 251 | */ 252 | function() { 253 | throw; 254 | } 255 | } -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.NODE_ENV = 'production'; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | var chalk = require('chalk'); 11 | var fs = require('fs-extra'); 12 | var path = require('path'); 13 | var pathExists = require('path-exists'); 14 | var filesize = require('filesize'); 15 | var gzipSize = require('gzip-size').sync; 16 | var webpack = require('webpack'); 17 | var config = require('../config/webpack.config.prod'); 18 | var paths = require('../config/paths'); 19 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | var recursive = require('recursive-readdir'); 21 | var stripAnsi = require('strip-ansi'); 22 | 23 | var useYarn = pathExists.sync(paths.yarnLockFile); 24 | 25 | // Warn and crash if required files are missing 26 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 27 | process.exit(1); 28 | } 29 | 30 | // Input: /User/dan/app/build/static/js/main.82be8.js 31 | // Output: /static/js/main.js 32 | function removeFileNameHash(fileName) { 33 | return fileName 34 | .replace(paths.appBuild, '') 35 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 36 | } 37 | 38 | // Input: 1024, 2048 39 | // Output: "(+1 KB)" 40 | function getDifferenceLabel(currentSize, previousSize) { 41 | var FIFTY_KILOBYTES = 1024 * 50; 42 | var difference = currentSize - previousSize; 43 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 44 | if (difference >= FIFTY_KILOBYTES) { 45 | return chalk.red('+' + fileSize); 46 | } else if (difference < FIFTY_KILOBYTES && difference > 0) { 47 | return chalk.yellow('+' + fileSize); 48 | } else if (difference < 0) { 49 | return chalk.green(fileSize); 50 | } else { 51 | return ''; 52 | } 53 | } 54 | 55 | // First, read the current file sizes in build directory. 56 | // This lets us display how much they changed later. 57 | recursive(paths.appBuild, (err, fileNames) => { 58 | var previousSizeMap = (fileNames || []) 59 | .filter(fileName => /\.(js|css)$/.test(fileName)) 60 | .reduce((memo, fileName) => { 61 | var contents = fs.readFileSync(fileName); 62 | var key = removeFileNameHash(fileName); 63 | memo[key] = gzipSize(contents); 64 | return memo; 65 | }, {}); 66 | 67 | // Remove all content but keep the directory so that 68 | // if you're in it, you don't end up in Trash 69 | fs.emptyDirSync(paths.appBuild); 70 | 71 | // Start the webpack build 72 | build(previousSizeMap); 73 | 74 | // Merge with the public folder 75 | copyPublicFolder(); 76 | }); 77 | 78 | // Print a detailed summary of build files. 79 | function printFileSizes(stats, previousSizeMap) { 80 | var assets = stats.toJson().assets 81 | .filter(asset => /\.(js|css)$/.test(asset.name)) 82 | .map(asset => { 83 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 84 | var size = gzipSize(fileContents); 85 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 86 | var difference = getDifferenceLabel(size, previousSize); 87 | return { 88 | folder: path.join('build', path.dirname(asset.name)), 89 | name: path.basename(asset.name), 90 | size: size, 91 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') 92 | }; 93 | }); 94 | assets.sort((a, b) => b.size - a.size); 95 | var longestSizeLabelLength = Math.max.apply(null, 96 | assets.map(a => stripAnsi(a.sizeLabel).length) 97 | ); 98 | assets.forEach(asset => { 99 | var sizeLabel = asset.sizeLabel; 100 | var sizeLength = stripAnsi(sizeLabel).length; 101 | if (sizeLength < longestSizeLabelLength) { 102 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 103 | sizeLabel += rightPadding; 104 | } 105 | console.log( 106 | ' ' + sizeLabel + 107 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 108 | ); 109 | }); 110 | } 111 | 112 | // Print out errors 113 | function printErrors(summary, errors) { 114 | console.log(chalk.red(summary)); 115 | console.log(); 116 | errors.forEach(err => { 117 | console.log(err.message || err); 118 | console.log(); 119 | }); 120 | } 121 | 122 | // Create the production build and print the deployment instructions. 123 | function build(previousSizeMap) { 124 | console.log('Creating an optimized production build...'); 125 | webpack(config).run((err, stats) => { 126 | if (err) { 127 | printErrors('Failed to compile.', [err]); 128 | process.exit(1); 129 | } 130 | 131 | if (stats.compilation.errors.length) { 132 | printErrors('Failed to compile.', stats.compilation.errors); 133 | process.exit(1); 134 | } 135 | 136 | if (process.env.CI && stats.compilation.warnings.length) { 137 | printErrors('Failed to compile.', stats.compilation.warnings); 138 | process.exit(1); 139 | } 140 | 141 | console.log(chalk.green('Compiled successfully.')); 142 | console.log(); 143 | 144 | console.log('File sizes after gzip:'); 145 | console.log(); 146 | printFileSizes(stats, previousSizeMap); 147 | console.log(); 148 | 149 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 150 | var appPackage = require(paths.appPackageJson); 151 | var homepagePath = appPackage.homepage; 152 | var publicPath = config.output.publicPath; 153 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 154 | // "homepage": "http://user.github.io/project" 155 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 156 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 157 | console.log(); 158 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 159 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); 160 | // If script deploy has been added to package.json, skip the instructions 161 | if (typeof appPackage.scripts.deploy === 'undefined') { 162 | console.log(); 163 | if (useYarn) { 164 | console.log(' ' + chalk.cyan('yarn') + ' add --dev gh-pages'); 165 | } else { 166 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); 167 | } 168 | console.log(); 169 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); 170 | console.log(); 171 | console.log(' ' + chalk.dim('// ...')); 172 | console.log(' ' + chalk.yellow('"scripts"') + ': {'); 173 | console.log(' ' + chalk.dim('// ...')); 174 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"npm run build&&gh-pages -d build"')); 175 | console.log(' }'); 176 | console.log(); 177 | console.log('Then run:'); 178 | } 179 | console.log(); 180 | console.log(' ' + chalk.cyan(useYarn ? 'yarn' : 'npm') + ' run deploy'); 181 | console.log(); 182 | } else if (publicPath !== '/') { 183 | // "homepage": "http://mywebsite.com/project" 184 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 185 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 186 | console.log(); 187 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 188 | console.log(); 189 | } else { 190 | // no homepage or "homepage": "http://mywebsite.com" 191 | console.log('The project was built assuming it is hosted at the server root.'); 192 | if (homepagePath) { 193 | // "homepage": "http://mywebsite.com" 194 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 195 | console.log(); 196 | } else { 197 | // no homepage 198 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); 199 | console.log('For example, add this to build it for GitHub Pages:') 200 | console.log(); 201 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); 202 | console.log(); 203 | } 204 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 205 | console.log('You may also serve it locally with a static server:') 206 | console.log(); 207 | if (useYarn) { 208 | console.log(' ' + chalk.cyan('yarn') + ' global add pushstate-server'); 209 | } else { 210 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); 211 | } 212 | console.log(' ' + chalk.cyan('pushstate-server') + ' build'); 213 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); 214 | console.log(); 215 | } 216 | }); 217 | } 218 | 219 | function copyPublicFolder() { 220 | fs.copySync(paths.appPublic, paths.appBuild, { 221 | dereference: true, 222 | filter: file => file !== paths.appHtml 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /src/api/web3Api.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import {getExtendedWeb3Provider} from '../utils/web3Utils'; 3 | import FundingHubContract from '../../build/contracts/FundingHub.json'; 4 | import ProjectContract from '../../build/contracts/Project.json'; 5 | 6 | const contract = require('truffle-contract'); 7 | 8 | let web3Provided; 9 | 10 | let provider; 11 | /* eslint-disable */ 12 | if (typeof web3 !== 'undefined') { 13 | provider = new Web3(web3.currentProvider); 14 | } else { 15 | provider = new Web3.providers.HttpProvider('http://localhost:8545'); 16 | } 17 | /* esling-enable */ 18 | 19 | const web3 = new Web3(provider); 20 | 21 | const fundingHub = contract(FundingHubContract); 22 | fundingHub.setProvider(provider); 23 | 24 | const project = contract(ProjectContract); 25 | project.setProvider(provider); 26 | 27 | /** 28 | * Check for a local web3, otherwise fallback to an infura instance 29 | */ 30 | function initializeWeb3() { 31 | /*eslint-disable */ 32 | if (typeof web3 !== 'undefined') { 33 | web3Provided = new Web3(web3.currentProvider); 34 | } else { 35 | web3Provided = new Web3(new Web3.providers.HttpProvider(testrpcUrl)); 36 | } 37 | /*eslint-enable */ 38 | 39 | return getExtendedWeb3Provider(web3Provided); 40 | } 41 | 42 | function web3Client() { 43 | if (web3Provided) { 44 | return web3Provided; 45 | } else { 46 | return initializeWeb3(); 47 | } 48 | } 49 | 50 | export function getAccounts() { 51 | return new Promise((resolve, reject) => { 52 | web3Client().eth.getAccounts(function (err, accts) { 53 | if (err != null) { 54 | console.log("Web3Api Error: ", err); 55 | reject(); 56 | } 57 | 58 | if (accts.length === 0) { 59 | console.log("Web3Api Error: couldn't get any accounts"); 60 | reject(); 61 | } 62 | 63 | let accountsAndBalances = accts.map((address => { 64 | return getAccountBalance(address).then((balance) => { 65 | return { address, balance} 66 | }); 67 | })); 68 | 69 | Promise.all(accountsAndBalances).then((accountsAndBalances) => { 70 | resolve(accountsAndBalances); 71 | }); 72 | 73 | }); 74 | 75 | }); 76 | } 77 | 78 | export function getAccountBalance(account) { 79 | return new Promise((resolve, reject) => { 80 | web3Client().eth.getBalance(account, function(err, value) { 81 | resolve(value.valueOf()); 82 | }); 83 | }); 84 | } 85 | 86 | export function getProjects() { 87 | return new Promise((resolve, reject) => { 88 | let fundingHubInstance; 89 | fundingHub.deployed().then(function(instance) { 90 | fundingHubInstance = instance; 91 | return fundingHubInstance.numOfProjects.call(); 92 | }).then(function(result) { 93 | console.log("getProjects: ", result); 94 | let projectCount = result.valueOf(); 95 | 96 | // create an array where length = projectCount 97 | let array = Array.apply(null, {length: projectCount}).map(Number.call, Number); 98 | 99 | // fill array with corresponding project contract addresses 100 | let projectPromises = array.map((id => { 101 | return getProjectAddress(id); 102 | })); 103 | 104 | // get projectDetails for each projectAddress promise 105 | Promise.all(projectPromises).then((projectAddresses) => { 106 | let projectDetailPromises = projectAddresses.map((address => { 107 | return getProjectDetails(address); 108 | })); 109 | 110 | Promise.all(projectDetailPromises).then((projects) => { 111 | resolve(projects); 112 | }); 113 | }); 114 | }); 115 | }); 116 | } 117 | 118 | function getProjectAddress(id) { 119 | return new Promise((resolve, reject) => { 120 | fundingHub.deployed().then(function(fundingHubInstance) { 121 | fundingHubInstance.projects.call(id).then(function(address) { 122 | resolve(address); 123 | }); 124 | }); 125 | }); 126 | } 127 | 128 | export function getProjectDetails(address) { 129 | return new Promise((resolve, reject) => { 130 | let projectInstance; 131 | project.at(address).then(function(instance) { 132 | projectInstance = instance; 133 | projectInstance.getProject.call().then(function(projectDetails) { 134 | resolve({ 135 | title: projectDetails[0], 136 | goal: fromWei(projectDetails[1].toNumber()), 137 | deadline: projectDetails[2].toNumber(), 138 | creator: projectDetails[3], 139 | totalFunding: fromWei(projectDetails[4].toNumber()), 140 | contributionsCount: projectDetails[5].toNumber(), 141 | contributorsCount: projectDetails[6].toNumber(), 142 | fundingHub: projectDetails[7], 143 | address: projectDetails[8] 144 | }); 145 | }); 146 | }); 147 | }); 148 | } 149 | 150 | export function createProject(params, creator) { 151 | return new Promise((resolve, reject) => { 152 | let fundingHubInstance; 153 | fundingHub.deployed().then(function(instance) { 154 | fundingHubInstance = instance; 155 | fundingHubInstance.createProject( 156 | toWei(params.projectGoalInEth), 157 | params.projectDeadline, 158 | params.projectName, 159 | { 160 | from: creator, 161 | gas: 1000000 162 | } 163 | ).then(function(tx) { 164 | console.log("web3Api.createProject() project tx: ", tx); 165 | resolve(tx); 166 | }); 167 | }); 168 | }); 169 | } 170 | 171 | export function contribute(contractAddr, amount, contributorAddr) { 172 | console.log("contractAddr: ", contractAddr); 173 | console.log("amount: ", amount); 174 | console.log("contributorAddr: ", contributorAddr); 175 | // let amt = parseInt(amount); // possible bug here? 176 | let amountInWei = toWei(amount); 177 | console.log("amountInWei: ", amountInWei); 178 | return new Promise((resolve, reject) => { 179 | fundingHub.deployed().then(function(instance) { 180 | // web3Client().eth.sendTransaction({ to: "0XF9AEEE7969452E1934BCD2067E570D813BDA8D52", value: toWei(amount), from: contributorAddr, gas: 3000000}, function(result) { 181 | // console.log(result); 182 | // resolve(result); 183 | // }); 184 | instance.contribute(contractAddr, { value: amountInWei, from: contributorAddr, gas: 3000000}) 185 | .then(function(resultObject) { 186 | console.log("web3Api.contribute() transaction result object: ", resultObject); 187 | resolve(resultObject); 188 | }); 189 | }); 190 | }); 191 | } 192 | 193 | export function getProjectContributions(address) { 194 | return new Promise((resolve, reject) => { 195 | project.at(address).then(function(instance) { 196 | instance.contributionsCount.call().then(function(num) { 197 | let contributionCount = num.valueOf(); 198 | 199 | let array = Array.apply(null, {length: contributionCount}).map(Number.call, Number); 200 | let contributionPromises = array.map((id => { 201 | return getContribution(address, id); 202 | })); 203 | 204 | Promise.all(contributionPromises).then((contributions) => { 205 | resolve(contributions); 206 | }); 207 | }); 208 | }); 209 | }); 210 | } 211 | 212 | function getContribution(projectAddress, id) { 213 | return new Promise((resolve, reject) => { 214 | project.at(projectAddress).then(function(instance) { 215 | instance.getContribution.call(id).then(function(contribution) { 216 | resolve({ 217 | amount: fromWei(contribution[0].toNumber()), 218 | contributor: contribution[1] 219 | }); 220 | }); 221 | }); 222 | }); 223 | } 224 | 225 | export function getAddressBalance(address) { 226 | return new Promise((resolve, reject) => { 227 | web3Client().eth.getBalance(address, function(err, value) { 228 | resolve(fromWei(value.valueOf())); 229 | }); 230 | }); 231 | } 232 | 233 | export function getCurrentBlockNumber() { 234 | return new Promise((resolve, reject) => { 235 | web3Client().eth.getBlockNumber(function (err, blockNum) { 236 | if (err) { 237 | reject(); 238 | } 239 | resolve(blockNum); 240 | }); 241 | }); 242 | } 243 | 244 | export function getNetwork() { 245 | return new Promise((resolve, reject) => { 246 | web3Client().version.getNetwork(function (err, network) { 247 | if (err) { 248 | reject(); 249 | } 250 | resolve(network); 251 | }) 252 | }) 253 | } 254 | 255 | export function toWei(ethValue) { 256 | return web3Client().toWei(ethValue, "ether"); 257 | } 258 | 259 | export function fromWei(weiValue) { 260 | return web3Client().fromWei(weiValue, "ether"); 261 | } -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var autoprefixer = require('autoprefixer'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 5 | var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 6 | var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 7 | var getClientEnvironment = require('./env'); 8 | var paths = require('./paths'); 9 | 10 | 11 | 12 | // Webpack uses `publicPath` to determine where the app is being served from. 13 | // In development, we always serve from the root. This makes config easier. 14 | var publicPath = '/'; 15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 18 | var publicUrl = ''; 19 | // Get environment variables to inject into our app. 20 | var env = getClientEnvironment(publicUrl); 21 | 22 | // This is the development configuration. 23 | // It is focused on developer experience and fast rebuilds. 24 | // The production configuration is different and lives in a separate file. 25 | module.exports = { 26 | // You may want 'eval' instead if you prefer to see the compiled output in DevTools. 27 | // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. 28 | devtool: 'cheap-module-source-map', 29 | // These are the "entry points" to our application. 30 | // This means they will be the "root" imports that are included in JS bundle. 31 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 32 | entry: [ 33 | // Include an alternative client for WebpackDevServer. A client's job is to 34 | // connect to WebpackDevServer by a socket and get notified about changes. 35 | // When you save a file, the client will either apply hot updates (in case 36 | // of CSS changes), or refresh the page (in case of JS changes). When you 37 | // make a syntax error, this client will display a syntax error overlay. 38 | // Note: instead of the default WebpackDevServer client, we use a custom one 39 | // to bring better experience for Create React App users. You can replace 40 | // the line below with these two lines if you prefer the stock client: 41 | // require.resolve('webpack-dev-server/client') + '?/', 42 | // require.resolve('webpack/hot/dev-server'), 43 | require.resolve('react-dev-utils/webpackHotDevClient'), 44 | // We ship a few polyfills by default: 45 | require.resolve('./polyfills'), 46 | // Finally, this is your app's code: 47 | paths.appIndexJs 48 | // We include the app code last so that if there is a runtime error during 49 | // initialization, it doesn't blow up the WebpackDevServer client, and 50 | // changing JS code would still trigger a refresh. 51 | ], 52 | output: { 53 | // Next line is not used in dev but WebpackDevServer crashes without it: 54 | path: paths.appBuild, 55 | // Add /* filename */ comments to generated require()s in the output. 56 | pathinfo: true, 57 | // This does not produce a real file. It's just the virtual path that is 58 | // served by WebpackDevServer in development. This is the JS bundle 59 | // containing code from all our entry points, and the Webpack runtime. 60 | filename: 'static/js/bundle.js', 61 | // This is the URL that app is served from. We use "/" in development. 62 | publicPath: publicPath 63 | }, 64 | resolve: { 65 | // This allows you to set a fallback for where Webpack should look for modules. 66 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 67 | // We use `fallback` instead of `root` because we want `node_modules` to "win" 68 | // if there any conflicts. This matches Node resolution mechanism. 69 | // https://github.com/facebookincubator/create-react-app/issues/253 70 | fallback: paths.nodePaths, 71 | // These are the reasonable defaults supported by the Node ecosystem. 72 | // We also include JSX as a common component filename extension to support 73 | // some tools, although we do not recommend using it, see: 74 | // https://github.com/facebookincubator/create-react-app/issues/290 75 | extensions: ['.js', '.json', '.jsx', ''], 76 | alias: { 77 | // Support React Native Web 78 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 79 | 'react-native': 'react-native-web' 80 | } 81 | }, 82 | 83 | module: { 84 | // First, run the linter. 85 | // It's important to do this before Babel processes the JS. 86 | preLoaders: [ 87 | { 88 | test: /\.(js|jsx)$/, 89 | loader: 'eslint', 90 | include: paths.appSrc, 91 | } 92 | ], 93 | loaders: [ 94 | // Default loader: load all assets that are not handled 95 | // by other loaders with the url loader. 96 | // Note: This list needs to be updated with every change of extensions 97 | // the other loaders match. 98 | // E.g., when adding a loader for a new supported file extension, 99 | // we need to add the supported extension to this loader too. 100 | // Add one new line in `exclude` for each loader. 101 | // 102 | // "file" loader makes sure those assets get served by WebpackDevServer. 103 | // When you `import` an asset, you get its (virtual) filename. 104 | // In production, they would get copied to the `build` folder. 105 | // "url" loader works like "file" loader except that it embeds assets 106 | // smaller than specified limit in bytes as data URLs to avoid requests. 107 | // A missing `test` is equivalent to a match. 108 | { 109 | exclude: [ 110 | /\.html$/, 111 | /\.(js|jsx)$/, 112 | /\.css$/, 113 | /\.json$/, 114 | /\.woff$/, 115 | /\.woff2$/, 116 | /\.(ttf|svg|eot)$/ 117 | ], 118 | loader: 'url', 119 | query: { 120 | limit: 10000, 121 | name: 'static/media/[name].[hash:8].[ext]' 122 | } 123 | }, 124 | // Process JS with Babel. 125 | { 126 | test: /\.(js|jsx)$/, 127 | include: [ 128 | paths.appSrc, 129 | ], 130 | loader: 'babel', 131 | query: { 132 | 133 | // This is a feature of `babel-loader` for webpack (not Babel itself). 134 | // It enables caching results in ./node_modules/.cache/babel-loader/ 135 | // directory for faster rebuilds. 136 | cacheDirectory: true 137 | } 138 | }, 139 | // "postcss" loader applies autoprefixer to our CSS. 140 | // "css" loader resolves paths in CSS and adds assets as dependencies. 141 | // "style" loader turns CSS into JS modules that inject