diff --git a/.travis.yml b/.travis.yml index 762c72c2..4ac831d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ matrix: - language: node_js node_js: 8.13.0 before_install: - - cd frontend/ + - cd frontend install: yarn script: - yarn run lint @@ -16,8 +16,7 @@ matrix: - cd backend/ - cp .env.example .env env: - - FLASK_APP=app.py FLASK_DEBUG=1 CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund - CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory + - CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory install: pip install -r requirements/dev.txt script: - flask test diff --git a/backend/.env.example b/backend/.env.example index 40df3430..bbeced82 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,12 +6,18 @@ DATABASE_URL="sqlite:////tmp/dev.db" REDISTOGO_URL="redis://localhost:6379" SECRET_KEY="not-so-secret" SENDGRID_API_KEY="optional, but emails won't send without it" + # for ropsten use the following # ETHEREUM_ENDPOINT_URI = "https://ropsten.infura.io/API_KEY" ETHEREUM_ENDPOINT_URI = "http://localhost:8545" -CROWD_FUND_URL = "https://eip-712.herokuapp.com/contract/crowd-fund" -CROWD_FUND_FACTORY_URL = "https://eip-712.herokuapp.com/contract/factory" + +# CROWD_FUND_URL = "https://eip-712.herokuapp.com/contract/crowd-fund" +# CROWD_FUND_FACTORY_URL = "https://eip-712.herokuapp.com/contract/factory" +CROWD_FUND_URL = "http://localhost:5000/dev-contracts/CrowdFund.json" +CROWD_FUND_FACTORY_URL = "http://localhost:5000/dev-contracts/CrowdFundFactory.json" + # SENTRY_DSN="https://PUBLICKEY@sentry.io/PROJECTID" # SENTRY_RELEASE="optional, overrides git hash" + UPLOAD_DIRECTORY = "/tmp" UPLOAD_URL = "http://localhost:5000" # for constructing download url diff --git a/backend/grant/app.py b/backend/grant/app.py index 35628322..03bc26f3 100644 --- a/backend/grant/app.py +++ b/backend/grant/app.py @@ -5,7 +5,7 @@ from flask_cors import CORS from sentry_sdk.integrations.flask import FlaskIntegration import sentry_sdk -from grant import commands, proposal, user, comment, milestone, admin, email +from grant import commands, proposal, user, comment, milestone, admin, email, web3 as web3module from grant.extensions import bcrypt, migrate, db, ma, mail, web3 from grant.settings import SENTRY_RELEASE, ENV @@ -46,6 +46,9 @@ def register_blueprints(app): app.register_blueprint(milestone.views.blueprint) app.register_blueprint(admin.views.blueprint) app.register_blueprint(email.views.blueprint) + # Only add these routes locally + if ENV == 'development': + app.register_blueprint(web3module.dev_contracts.blueprint) def register_shellcontext(app): diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index 3d2e0e39..0aaa5a61 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -19,6 +19,7 @@ from .models import( proposal_contribution_schema, db ) +import traceback blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals") @@ -88,12 +89,16 @@ def get_proposals(stage): else: proposals = Proposal.query.order_by(Proposal.date_created.desc()).all() dumped_proposals = proposals_schema.dump(proposals) - for p in dumped_proposals: - proposal_contract = read_proposal(p['proposal_address']) - p['crowd_fund'] = proposal_contract - filtered_proposals = list(filter(lambda p: p['crowd_fund'] is not None, dumped_proposals)) - return filtered_proposals - + try: + for p in dumped_proposals: + proposal_contract = read_proposal(p['proposal_address']) + p['crowd_fund'] = proposal_contract + filtered_proposals = list(filter(lambda p: p['crowd_fund'] is not None, dumped_proposals)) + return filtered_proposals + except Exception as e: + print(e) + print(traceback.format_exc()) + return {"message": "Oops! Something went wrong."}, 500 @blueprint.route("/", methods=["POST"]) @requires_sm diff --git a/backend/grant/settings.py b/backend/grant/settings.py index 210b5642..11b14a5e 100644 --- a/backend/grant/settings.py +++ b/backend/grant/settings.py @@ -9,7 +9,11 @@ environment variables. import subprocess from environs import Env -git_revision_short_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']) +def git_revision_short_hash(): + try: + return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']) + except subprocess.CalledProcessError: + return 0 env = Env() env.read_env() @@ -33,7 +37,7 @@ SENDGRID_DEFAULT_FROM = "noreply@grant.io" ETHEREUM_PROVIDER = "http" ETHEREUM_ENDPOINT_URI = env.str("ETHEREUM_ENDPOINT_URI") SENTRY_DSN = env.str("SENTRY_DSN", default=None) -SENTRY_RELEASE = env.str("SENTRY_RELEASE", default=git_revision_short_hash) +SENTRY_RELEASE = env.str("SENTRY_RELEASE", default=git_revision_short_hash()) UPLOAD_DIRECTORY = env.str("UPLOAD_DIRECTORY") UPLOAD_URL = env.str("UPLOAD_URL") MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5MB (limits file uploads, raises RequestEntityTooLarge) diff --git a/backend/grant/web3/__init__.py b/backend/grant/web3/__init__.py index e69de29b..dc348e3a 100644 --- a/backend/grant/web3/__init__.py +++ b/backend/grant/web3/__init__.py @@ -0,0 +1 @@ +from . import dev_contracts \ No newline at end of file diff --git a/backend/grant/web3/dev_contracts.py b/backend/grant/web3/dev_contracts.py new file mode 100644 index 00000000..465930bf --- /dev/null +++ b/backend/grant/web3/dev_contracts.py @@ -0,0 +1,19 @@ +import json + +from flask import Blueprint, jsonify + +blueprint = Blueprint('dev-contracts', __name__, url_prefix='/dev-contracts') + + +@blueprint.route("/CrowdFundFactory.json", methods=["GET"]) +def factory(): + with open("../contract/build/contracts/CrowdFundFactory.json", "r") as read_file: + crowd_fund_factory_json = json.load(read_file) + return jsonify(crowd_fund_factory_json) + + +@blueprint.route("/CrowdFund.json", methods=["GET"]) +def crowd_find(): + with open("../contract/build/contracts/CrowdFund.json", "r") as read_file: + crowd_fund_json = json.load(read_file) + return jsonify(crowd_fund_json) diff --git a/backend/grant/web3/proposal.py b/backend/grant/web3/proposal.py index b67c36cc..c1868977 100644 --- a/backend/grant/web3/proposal.py +++ b/backend/grant/web3/proposal.py @@ -1,10 +1,10 @@ -import json import time -from flask_web3 import current_web3 -from .util import batch_call, call_array, RpcError -import requests -from grant.settings import CROWD_FUND_URL +import requests +from flask_web3 import current_web3 + +from grant.settings import CROWD_FUND_URL +from .util import batch_call, call_array, RpcError crowd_fund_abi = None @@ -14,19 +14,14 @@ def get_crowd_fund_abi(): if crowd_fund_abi: return crowd_fund_abi - if CROWD_FUND_URL: - crowd_fund_json = requests.get(CROWD_FUND_URL).json() - crowd_fund_abi = crowd_fund_json['abi'] - return crowd_fund_abi - - with open("../contract/build/contracts/CrowdFund.json", "r") as read_file: - crowd_fund_abi = json.load(read_file)['abi'] - return crowd_fund_abi - + crowd_fund_json = requests.get(CROWD_FUND_URL).json() + crowd_fund_abi = crowd_fund_json['abi'] + return crowd_fund_abi def read_proposal(address): - current_web3.eth.defaultAccount = current_web3.eth.accounts[0] + current_web3.eth.defaultAccount = '0x537680D921C000fC52Af9962ceEb4e359C50F424' if not current_web3.eth.accounts else \ + current_web3.eth.accounts[0] crowd_fund_abi = get_crowd_fund_abi() contract = current_web3.eth.contract(address=address, abi=crowd_fund_abi) @@ -106,6 +101,7 @@ def read_proposal(address): def get_no_vote(i): return derived_results['getContributorMilestoneVote' + contrib_address + str(i)] + no_votes = list(map(get_no_vote, range(len(crowd_fund['milestones'])))) contrib = { diff --git a/backend/tests/web3/test_proposal_read.py b/backend/tests/web3/test_proposal_read.py index 344d5282..634da15f 100644 --- a/backend/tests/web3/test_proposal_read.py +++ b/backend/tests/web3/test_proposal_read.py @@ -1,14 +1,14 @@ -import json import time import eth_tester.backends.pyevm.main as py_evm_main +import requests from flask_web3 import current_web3 + from grant.extensions import web3 from grant.settings import CROWD_FUND_URL, CROWD_FUND_FACTORY_URL from grant.web3.proposal import read_proposal - from ..config import BaseTestConfig -import requests + # increase gas limit on eth-tester # https://github.com/ethereum/web3.py/issues/1013 # https://gitter.im/ethereum/py-evm?at=5b7eb68c4be56c5918854337 @@ -24,17 +24,8 @@ class TestWeb3ProposalRead(BaseTestConfig): BaseTestConfig.setUp(self) # the following will properly configure web3 with test config web3.init_app(self.real_app) - if CROWD_FUND_FACTORY_URL: - crowd_fund_factory_json = requests.get(CROWD_FUND_FACTORY_URL).json() - else: - with open("../frontend/client/lib/contracts/CrowdFundFactory.json", "r") as read_file: - crowd_fund_factory_json = json.load(read_file) - - if CROWD_FUND_URL: - self.crowd_fund_json = requests.get(CROWD_FUND_URL).json() - else: - with open("../frontend/client/lib/contracts/CrowdFund.json", "r") as read_file: - self.crowd_fund_json = json.load(read_file) + crowd_fund_factory_json = requests.get(CROWD_FUND_FACTORY_URL).json() + self.crowd_fund_json = requests.get(CROWD_FUND_URL).json() current_web3.eth.defaultAccount = current_web3.eth.accounts[0] CrowdFundFactory = current_web3.eth.contract( abi=crowd_fund_factory_json['abi'], bytecode=crowd_fund_factory_json['bytecode']) diff --git a/contract/.nvmrc b/contract/.nvmrc new file mode 100644 index 00000000..85943544 --- /dev/null +++ b/contract/.nvmrc @@ -0,0 +1 @@ +8.13.0 \ No newline at end of file diff --git a/frontend/.envexample b/frontend/.envexample index 74862750..6c76bffc 100644 --- a/frontend/.envexample +++ b/frontend/.envexample @@ -4,14 +4,19 @@ FUND_ETH_ADDRESSES=0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520,0xDECAF9CD2367cdbb # Disable typescript checking for dev building (reduce build time & resource usage) NO_DEV_TS_CHECK=true +NODE_ENV=development + # Set the public host url (no trailing slash) PUBLIC_HOST_URL=https://demo.grant.io +BACKEND_URL=http://localhost:5000 # sentry -SENTRY_DSN="https://PUBLICKEY@sentry.io/PROJECTID" +SENTRY_DSN=https://PUBLICKEY@sentry.io/PROJECTID SENTRY_RELEASE="optional, overrides git hash" -CROWD_FUND_URL = "https://eip-712.herokuapp.com/contract/crowd-fund" -CROWD_FUND_FACTORY_URL = "https://eip-712.herokuapp.com/contract/factory" +# CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund +# CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory +CROWD_FUND_URL=http://localhost:5000/dev-contracts/CrowdFund.json +CROWD_FUND_FACTORY_URL=http://localhost:5000/dev-contracts/CrowdFundFactory.json \ No newline at end of file diff --git a/frontend/bin/truffle-util.js b/frontend/bin/truffle-util.js index 8ddca4b4..203632e3 100644 --- a/frontend/bin/truffle-util.js +++ b/frontend/bin/truffle-util.js @@ -12,16 +12,30 @@ require('../config/env'); module.exports = {}; +const CHECK_CONTRACT_IDS = ['CrowdFundFactory.json'] + const clean = (module.exports.clean = () => { rimraf.sync(paths.contractsBuild); }); const compile = (module.exports.compile = () => { - childProcess.execSync('yarn build', { cwd: paths.contractsBase }); + logMessage('truffle compile, please wait...', 'info'); + try { + childProcess.execSync('yarn build', { cwd: paths.contractsBase }); + } catch (e) { + logMessage(e.stdout.toString('utf8'), 'error'); + process.exit(1); + } }); const migrate = (module.exports.migrate = () => { - childProcess.execSync('truffle migrate', { cwd: paths.contractsBase }); + logMessage('truffle migrate, please wait...', 'info'); + try { + childProcess.execSync('truffle migrate', { cwd: paths.contractsBase }); + } catch (e) { + logMessage(e.stdout.toString('utf8'), 'error'); + process.exit(1); + } }); const makeWeb3Conn = () => { @@ -62,20 +76,41 @@ const getGanacheNetworkId = (module.exports.getGanacheNetworkId = () => { .catch(() => -1); }); -const checkContractsNetworkIds = (module.exports.checkContractsNetworkIds = id => +const checkContractsNetworkIds = (module.exports.checkContractsNetworkIds = ( + id, + retry = false, +) => new Promise((res, rej) => { const buildDir = paths.contractsBuild; - fs.readdir(buildDir, (err, names) => { + fs.readdir(buildDir, (err) => { if (err) { logMessage(`No contracts build directory @ ${buildDir}`, 'error'); res(false); } else { - const allHaveId = names.reduce((ok, name) => { + const allHaveId = CHECK_CONTRACT_IDS.reduce((ok, name) => { const contract = require(path.join(buildDir, name)); - if (Object.keys(contract.networks).length > 0 && !contract.networks[id]) { - const actual = Object.keys(contract.networks).join(', '); - logMessage(`${name} should have networks[${id}], it has ${actual}`, 'error'); - return false; + const contractHasKeys = Object.keys(contract.networks).length > 0; + if (!contractHasKeys) { + if (retry) { + logMessage( + 'Contract does not contain network keys after retry. Exiting. Please manually debug Contract JSON.', + 'error', + ); + process.exit(1); + } else { + logMessage('Contract does not contain any keys. Will migrate.'); + migrate(); + return checkContractsNetworkIds(id, true); + } + } else { + if (contractHasKeys && !contract.networks[id]) { + const actual = Object.keys(contract.networks).join(', '); + logMessage( + `${name} should have networks[${id}], it has ${actual}`, + 'error', + ); + return false; + } } return true && ok; }, true); @@ -128,9 +163,7 @@ module.exports.ethereumCheck = () => if (!allHaveId) { logMessage('Contract problems, will compile & migrate.', 'warning'); clean(); - logMessage('truffle compile, please wait...', 'info'); compile(); - logMessage('truffle migrate, please wait...', 'info'); migrate(); fundWeb3v1(); } else { diff --git a/frontend/client/lib/crowdFundContracts.ts b/frontend/client/lib/crowdFundContracts.ts index 679765a9..cd8d420c 100644 --- a/frontend/client/lib/crowdFundContracts.ts +++ b/frontend/client/lib/crowdFundContracts.ts @@ -10,11 +10,7 @@ export async function getCrowdFundContract(web3: Web3 | null, deployedAddress: s } if (!contractCache[deployedAddress]) { let CrowdFund; - if (process.env.CROWD_FUND_FACTORY_URL) { - CrowdFund = await fetchCrowdFundJSON(); - } else { - CrowdFund = await import('./contracts/CrowdFund.json'); - } + CrowdFund = await fetchCrowdFundJSON(); try { contractCache[deployedAddress] = await getContractInstance( web3, diff --git a/frontend/client/lib/getContract.ts b/frontend/client/lib/getContract.ts index 67dcf31a..58cccd80 100644 --- a/frontend/client/lib/getContract.ts +++ b/frontend/client/lib/getContract.ts @@ -10,7 +10,11 @@ const getContractInstance = async ( // get network ID and the deployed address const networkId = await web3.eth.net.getId(); if (!deployedAddress && !contractDefinition.networks[networkId]) { - throw new WrongNetworkError('Wrong web3 network configured'); + throw new WrongNetworkError( + `Wrong web3 network configured. Deployed address: ${deployedAddress}; networkId: ${networkId}, contractDefinitionNetworks: ${JSON.stringify( + contractDefinition.networks, + )}`, + ); } deployedAddress = deployedAddress || contractDefinition.networks[networkId].address; diff --git a/frontend/client/modules/web3/sagas.ts b/frontend/client/modules/web3/sagas.ts index 01bbea98..95ce1765 100644 --- a/frontend/client/modules/web3/sagas.ts +++ b/frontend/client/modules/web3/sagas.ts @@ -6,18 +6,12 @@ import { safeEnable } from 'utils/web3'; import types from './types'; import { fetchCrowdFundFactoryJSON } from 'api/api'; -/* tslint:disable no-var-requires --- TODO: find a better way to import contract */ -let CrowdFundFactory = require('lib/contracts/CrowdFundFactory.json'); - export function* bootstrapWeb3(): SagaIterator { // Don't attempt to bootstrap web3 on SSR if (process.env.SERVER_SIDE_RENDER) { return; } - if (process.env.CROWD_FUND_FACTORY_URL) { - CrowdFundFactory = yield call(fetchCrowdFundFactoryJSON); - } - + const CrowdFundFactory = yield call(fetchCrowdFundFactoryJSON); yield put(setWeb3()); yield take(types.WEB3_FULFILLED); diff --git a/frontend/config/env.js b/frontend/config/env.js index 34af2c06..e45f5050 100644 --- a/frontend/config/env.js +++ b/frontend/config/env.js @@ -2,6 +2,8 @@ const fs = require('fs'); const path = require('path'); const paths = require('./paths'); const childProcess = require('child_process'); +const dotenv = require('dotenv'); +const { logMessage } = require('../bin/utils'); delete require.cache[require.resolve('./paths')]; @@ -11,21 +13,17 @@ if (!process.env.NODE_ENV) { ); } -// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -const dotenvFiles = [ - `${paths.dotenv}.${process.env.NODE_ENV}.local`, - `${paths.dotenv}.${process.env.NODE_ENV}`, - process.env.NODE_ENV !== 'test' && `${paths.dotenv}.local`, - paths.dotenv, -].filter(Boolean); - -dotenvFiles.forEach(dotenvFile => { - if (fs.existsSync(dotenvFile)) { - require('dotenv').config({ - path: dotenvFile, - }); +// Override local ENV variables with .env +if (fs.existsSync(paths.dotenv)) { + const envConfig = dotenv.parse(fs.readFileSync(paths.dotenv)); + // tslint:disable-next-line + for (const k in envConfig) { + if (process.env[k]) { + logMessage(`Warning! Over-writing existing ENV Variable ${k}`); + } + process.env[k] = envConfig[k]; } -}); +} const envProductionRequiredHandler = (envVariable, fallbackValue) => { if (!process.env[envVariable]) { @@ -72,6 +70,8 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '') module.exports = () => { const raw = { BACKEND_URL: process.env.BACKEND_URL, + CROWD_FUND_FACTORY_URL: process.env.CROWD_FUND_FACTORY_URL, + CROWD_FUND_URL: process.env.CROWD_FUND_URL, NODE_ENV: process.env.NODE_ENV || 'development', PORT: process.env.PORT || 3000, PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL, diff --git a/frontend/config/webpack.config.js/module-dependency-warning.js b/frontend/config/webpack.config.js/module-dependency-warning.js index f1b49560..dd1e5da5 100644 --- a/frontend/config/webpack.config.js/module-dependency-warning.js +++ b/frontend/config/webpack.config.js/module-dependency-warning.js @@ -1,6 +1,6 @@ const ModuleDependencyWarning = require('webpack/lib/ModuleDependencyWarning'); -// supress unfortunate warnings due to transpileOnly=true and certain ts export patterns +// suppress unfortunate warnings due to transpileOnly=true and certain ts export patterns // https://github.com/TypeStrong/ts-loader/issues/653#issuecomment-390889335 // https://github.com/TypeStrong/ts-loader/issues/751 diff --git a/frontend/package.json b/frontend/package.json index b7ea7db7..9f22ce6e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,10 +4,9 @@ "main": "index.js", "license": "MIT", "scripts": { - "analyze": "NODE_ENV=production ANALYZE=true next build ./client", - "build": "cross-env NODE_ENV=production node bin/build.js", - "dev": "rm -rf ./node_modules/.cache && cross-env NODE_ENV=development BACKEND_URL=http://localhost:5000 node bin/dev.js", - "lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\"", + "build": "node bin/build.js", + "dev": "rm -rf ./node_modules/.cache && node bin/dev.js", + "lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\" -e \"**/bin/**\"", "start": "NODE_ENV=production node ./build/server/server.js", "now": "npm run build && now -e BACKEND_URL=https://grant-stage.herokuapp.com", "heroku-postbuild": "yarn build", @@ -90,7 +89,6 @@ "copy-webpack-plugin": "^4.6.0", "core-js": "^2.5.7", "cors": "^2.8.4", - "cross-env": "^5.2.0", "css-loader": "^1.0.0", "dotenv": "^6.0.0", "ethereum-blockies-base64": "1.0.2", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8155de23..92f86eec 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4838,13 +4838,6 @@ cropperjs@v1.0.0-rc.3: version "1.0.0-rc.3" resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.0.0-rc.3.tgz#50a7c7611befc442702f845ede77d7df4572e82b" -cross-env@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" - dependencies: - cross-spawn "^6.0.5" - is-windows "^1.0.0" - cross-spawn@5.1.0, cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -8459,7 +8452,7 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2: +is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"