Merge develop in.
This commit is contained in:
commit
5afdb2011d
|
@ -1 +1,2 @@
|
|||
.idea
|
||||
contract/build
|
13
.travis.yml
13
.travis.yml
|
@ -2,9 +2,9 @@ matrix:
|
|||
include:
|
||||
# Frontend
|
||||
- language: node_js
|
||||
node_js: 8.11.4
|
||||
node_js: 8.13.0
|
||||
before_install:
|
||||
- cd frontend/
|
||||
- cd frontend
|
||||
install: yarn
|
||||
script:
|
||||
- yarn run lint
|
||||
|
@ -15,17 +15,22 @@ matrix:
|
|||
before_install:
|
||||
- cd backend/
|
||||
- cp .env.example .env
|
||||
env:
|
||||
- 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
|
||||
# Contracts
|
||||
- language: node_js
|
||||
node_js: 8.11.4
|
||||
node_js: 8.13.0
|
||||
before_install:
|
||||
- cd contract/
|
||||
install: yarn && yarn add global truffle ganache-cli
|
||||
install: yarn && yarn add global truffle ganache-cli@6.1.8
|
||||
before_script:
|
||||
- ganache-cli > /dev/null &
|
||||
- sleep 10
|
||||
script:
|
||||
- yarn run test
|
||||
env:
|
||||
- CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund
|
||||
CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory
|
||||
|
|
|
@ -6,3 +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 = "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
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Config file for automatic testing at travis-ci.org
|
||||
sudo: false # http://docs.travis-ci.com/user/migrating-from-legacy/
|
||||
language: python
|
||||
env:
|
||||
- FLASK_APP=app.py FLASK_DEBUG=1
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
install:
|
||||
- pip install -r requirements/dev.txt
|
||||
- nvm install 6.10
|
||||
- nvm use 6.10
|
||||
- npm install
|
||||
before_script:
|
||||
- npm run lint
|
||||
- npm run build
|
||||
- flask lint
|
||||
script: flask test
|
|
@ -2,9 +2,12 @@
|
|||
"""The app module, containing the app factory function."""
|
||||
from flask import Flask
|
||||
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.extensions import bcrypt, migrate, db, ma, mail
|
||||
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
|
||||
|
||||
|
||||
def create_app(config_object="grant.settings"):
|
||||
|
@ -15,6 +18,11 @@ def create_app(config_object="grant.settings"):
|
|||
register_blueprints(app)
|
||||
register_shellcontext(app)
|
||||
register_commands(app)
|
||||
sentry_sdk.init(
|
||||
environment=ENV,
|
||||
release=SENTRY_RELEASE,
|
||||
integrations=[FlaskIntegration()]
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
|
@ -25,6 +33,7 @@ def register_extensions(app):
|
|||
migrate.init_app(app, db)
|
||||
ma.init_app(app)
|
||||
mail.init_app(app)
|
||||
web3.init_app(app)
|
||||
CORS(app)
|
||||
return None
|
||||
|
||||
|
@ -37,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):
|
||||
|
|
|
@ -5,9 +5,11 @@ from flask_marshmallow import Marshmallow
|
|||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_sendgrid import SendGrid
|
||||
from flask_web3 import FlaskWeb3
|
||||
|
||||
bcrypt = Bcrypt()
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
ma = Marshmallow()
|
||||
mail = SendGrid()
|
||||
web3 = FlaskWeb3()
|
||||
|
|
|
@ -45,6 +45,33 @@ class ProposalUpdate(db.Model):
|
|||
self.date_created = datetime.datetime.now()
|
||||
|
||||
|
||||
class ProposalContribution(db.Model):
|
||||
__tablename__ = "proposal_contribution"
|
||||
|
||||
tx_id = db.Column(db.String(255), primary_key=True)
|
||||
date_created = db.Column(db.DateTime, nullable=False)
|
||||
|
||||
proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||
from_address = db.Column(db.String(255), nullable=False)
|
||||
amount = db.Column(db.String(255), nullable=False) # in eth
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tx_id: str,
|
||||
proposal_id: int,
|
||||
user_id: int,
|
||||
from_address: str,
|
||||
amount: str
|
||||
):
|
||||
self.tx_id = tx_id
|
||||
self.proposal_id = proposal_id
|
||||
self.user_id = user_id
|
||||
self.from_address = from_address
|
||||
self.amount = amount
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
|
||||
class Proposal(db.Model):
|
||||
__tablename__ = "proposal"
|
||||
|
||||
|
@ -60,6 +87,7 @@ class Proposal(db.Model):
|
|||
team = db.relationship("User", secondary=proposal_team)
|
||||
comments = db.relationship(Comment, backref="proposal", lazy=True)
|
||||
updates = db.relationship(ProposalUpdate, backref="proposal", lazy=True)
|
||||
contributions = db.relationship(ProposalContribution, backref="proposal", lazy=True)
|
||||
milestones = db.relationship("Milestone", backref="proposal", lazy=True)
|
||||
|
||||
def __init__(
|
||||
|
@ -110,6 +138,7 @@ class ProposalSchema(ma.Schema):
|
|||
"body",
|
||||
"comments",
|
||||
"updates",
|
||||
"contributions",
|
||||
"milestones",
|
||||
"category",
|
||||
"team"
|
||||
|
@ -121,6 +150,7 @@ class ProposalSchema(ma.Schema):
|
|||
|
||||
comments = ma.Nested("CommentSchema", many=True)
|
||||
updates = ma.Nested("ProposalUpdateSchema", many=True)
|
||||
contributions = ma.Nested("ProposalContributionSchema", many=True)
|
||||
team = ma.Nested("UserSchema", many=True)
|
||||
milestones = ma.Nested("MilestoneSchema", many=True)
|
||||
|
||||
|
@ -166,3 +196,30 @@ class ProposalUpdateSchema(ma.Schema):
|
|||
|
||||
proposal_update_schema = ProposalUpdateSchema()
|
||||
proposals_update_schema = ProposalUpdateSchema(many=True)
|
||||
|
||||
|
||||
class ProposalContributionSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = ProposalContribution
|
||||
# Fields to expose
|
||||
fields = (
|
||||
"id",
|
||||
"tx_id",
|
||||
"proposal_id",
|
||||
"user_id",
|
||||
"from_address",
|
||||
"amount",
|
||||
"date_created",
|
||||
)
|
||||
id = ma.Method("get_id")
|
||||
date_created = ma.Method("get_date_created")
|
||||
|
||||
def get_id(self, obj):
|
||||
return obj.tx_id
|
||||
|
||||
def get_date_created(self, obj):
|
||||
return dt_to_unix(obj.date_created)
|
||||
|
||||
|
||||
proposal_contribution_schema = ProposalContributionSchema()
|
||||
proposals_contribution_schema = ProposalContributionSchema(many=True)
|
||||
|
|
|
@ -10,7 +10,18 @@ from grant.comment.models import Comment, comment_schema, comments_schema
|
|||
from grant.milestone.models import Milestone
|
||||
from grant.user.models import User, SocialMedia, Avatar
|
||||
from grant.utils.auth import requires_sm, requires_team_member_auth, verify_signed_auth, BadSignatureException
|
||||
from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, db
|
||||
from grant.web3.proposal import read_proposal, validate_contribution_tx
|
||||
from .models import(
|
||||
Proposal,
|
||||
proposals_schema,
|
||||
proposal_schema,
|
||||
ProposalUpdate,
|
||||
proposal_update_schema,
|
||||
ProposalContribution,
|
||||
proposal_contribution_schema,
|
||||
db
|
||||
)
|
||||
import traceback
|
||||
|
||||
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
||||
|
||||
|
@ -21,6 +32,10 @@ def get_proposal(proposal_id):
|
|||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
dumped_proposal = proposal_schema.dump(proposal)
|
||||
proposal_contract = read_proposal(dumped_proposal['proposal_address'])
|
||||
if not proposal_contract:
|
||||
return {"message": "Proposal retired"}, 404
|
||||
dumped_proposal['crowd_fund'] = proposal_contract
|
||||
return dumped_proposal
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
@ -108,8 +123,16 @@ def get_proposals(stage):
|
|||
else:
|
||||
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
|
||||
dumped_proposals = proposals_schema.dump(proposals)
|
||||
return dumped_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
|
||||
|
@ -234,3 +257,52 @@ def post_proposal_update(proposal_id, title, content):
|
|||
|
||||
dumped_update = proposal_update_schema.dump(update)
|
||||
return dumped_update, 201
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_contributions(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
dumped_proposal = proposal_schema.dump(proposal)
|
||||
return dumped_proposal["contributions"]
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions/<contribution_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_contribution(proposal_id, contribution_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
contribution = ProposalContribution.query.filter_by(tx_id=contribution_id).first()
|
||||
if contribution:
|
||||
return proposal_contribution_schema.dump(contribution)
|
||||
else:
|
||||
return {"message": "No contribution matching id"}
|
||||
else:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
||||
@requires_sm
|
||||
@endpoint.api(
|
||||
parameter('txId', type=str, required=True),
|
||||
parameter('fromAddress', type=str, required=True),
|
||||
parameter('amount', type=str, required=True)
|
||||
)
|
||||
def post_proposal_contribution(proposal_id, tx_id, from_address, amount):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
contribution = ProposalContribution(
|
||||
tx_id=tx_id,
|
||||
proposal_id=proposal_id,
|
||||
user_id=g.current_user.id,
|
||||
from_address=from_address,
|
||||
amount=amount
|
||||
)
|
||||
db.session.add(contribution)
|
||||
db.session.commit()
|
||||
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
||||
return dumped_contribution, 201
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
|
|
@ -6,8 +6,15 @@ Most configuration is set via environment variables.
|
|||
For local development, use a .env file to set
|
||||
environment variables.
|
||||
"""
|
||||
import subprocess
|
||||
from environs import Env
|
||||
|
||||
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()
|
||||
|
||||
|
@ -15,6 +22,8 @@ ENV = env.str("FLASK_ENV", default="production")
|
|||
DEBUG = ENV == "development"
|
||||
SITE_URL = env.str('SITE_URL', default='https://grant.io')
|
||||
AUTH_URL = env.str('AUTH_URL', default='https://eip-712.herokuapp.com')
|
||||
CROWD_FUND_FACTORY_URL = env.str('CROWD_FUND_FACTORY_URL', default=None)
|
||||
CROWD_FUND_URL = env.str('CROWD_FUND_URL', default=None)
|
||||
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
|
||||
QUEUES = ["default"]
|
||||
SECRET_KEY = env.str("SECRET_KEY")
|
||||
|
@ -25,3 +34,10 @@ CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
|
|||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SENDGRID_API_KEY = env.str("SENDGRID_API_KEY", default="")
|
||||
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())
|
||||
UPLOAD_DIRECTORY = env.str("UPLOAD_DIRECTORY")
|
||||
UPLOAD_URL = env.str("UPLOAD_URL")
|
||||
MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5MB (limits file uploads, raises RequestEntityTooLarge)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from flask import Blueprint, g
|
||||
from flask import Blueprint, g, request
|
||||
from flask_yoloapi import endpoint, parameter
|
||||
|
||||
from grant.proposal.models import Proposal, proposal_team
|
||||
from grant.utils.auth import requires_sm, requires_same_user_auth, verify_signed_auth, BadSignatureException
|
||||
from grant.utils.upload import save_avatar, send_upload, remove_avatar
|
||||
from grant.settings import UPLOAD_URL
|
||||
from .models import User, SocialMedia, Avatar, users_schema, user_schema, db
|
||||
|
||||
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
|
||||
|
@ -114,6 +116,38 @@ def auth_user(account_address, signed_message, raw_typed_data):
|
|||
return user_schema.dump(existing_user)
|
||||
|
||||
|
||||
@blueprint.route("/avatar", methods=["POST"])
|
||||
@requires_sm
|
||||
@endpoint.api()
|
||||
def upload_avatar():
|
||||
user = g.current_user
|
||||
if 'file' not in request.files:
|
||||
return {"message": "No file in post"}, 400
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return {"message": "No selected file"}, 400
|
||||
try:
|
||||
filename = save_avatar(file, user.id)
|
||||
return {"url": "{0}/api/v1/users/avatar/{1}".format(UPLOAD_URL, filename)}
|
||||
except Exception as e:
|
||||
return {"message": str(e)}, 400
|
||||
|
||||
|
||||
@blueprint.route("/avatar/<filename>", methods=["GET"])
|
||||
def get_avatar(filename):
|
||||
return send_upload(filename)
|
||||
|
||||
|
||||
@blueprint.route("/avatar", methods=["DELETE"])
|
||||
@requires_sm
|
||||
@endpoint.api(
|
||||
parameter('url', type=str, required=True)
|
||||
)
|
||||
def delete_avatar(url):
|
||||
user = g.current_user
|
||||
remove_avatar(url, user.id)
|
||||
|
||||
|
||||
@blueprint.route("/<user_identity>", methods=["PUT"])
|
||||
@requires_sm
|
||||
@requires_same_user_auth
|
||||
|
@ -140,6 +174,7 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
|
|||
else:
|
||||
SocialMedia.query.filter_by(user_id=user.id).delete()
|
||||
|
||||
old_avatar = Avatar.query.filter_by(user_id=user.id).first()
|
||||
if avatar is not None:
|
||||
Avatar.query.filter_by(user_id=user.id).delete()
|
||||
avatar_link = avatar.get('link')
|
||||
|
@ -149,6 +184,11 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
|
|||
else:
|
||||
Avatar.query.filter_by(user_id=user.id).delete()
|
||||
|
||||
old_avatar_url = old_avatar and old_avatar.image_url
|
||||
new_avatar_url = avatar and avatar['link']
|
||||
if old_avatar_url and old_avatar_url != new_avatar_url:
|
||||
remove_avatar(old_avatar_url, user.id)
|
||||
|
||||
db.session.commit()
|
||||
result = user_schema.dump(user)
|
||||
return result
|
||||
|
|
|
@ -6,6 +6,7 @@ import requests
|
|||
from flask import request, g, jsonify
|
||||
from itsdangerous import SignatureExpired, BadSignature
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
|
||||
import sentry_sdk
|
||||
|
||||
from grant.settings import SECRET_KEY, AUTH_URL
|
||||
from ..proposal.models import Proposal
|
||||
|
@ -72,6 +73,10 @@ def requires_sm(f):
|
|||
return jsonify(message="No user exists with address: {}".format(auth_address)), 401
|
||||
|
||||
g.current_user = user
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
scope.user = {
|
||||
"id": user.id,
|
||||
}
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return jsonify(message="Authentication is required to access this resource"), 401
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import os
|
||||
import re
|
||||
from hashlib import md5
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import send_from_directory
|
||||
from grant.settings import UPLOAD_DIRECTORY
|
||||
|
||||
IMAGE_MIME_TYPES = set(['image/png', 'image/jpg', 'image/gif'])
|
||||
AVATAR_MAX_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
|
||||
class FileValidationException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def allowed_avatar_file(file):
|
||||
if file.mimetype not in IMAGE_MIME_TYPES:
|
||||
raise FileValidationException("Unacceptable file type: {0}".format(file.mimetype))
|
||||
file.seek(0, os.SEEK_END)
|
||||
size = file.tell()
|
||||
file.seek(0)
|
||||
if size > AVATAR_MAX_SIZE:
|
||||
raise FileValidationException(
|
||||
"File size is too large ({0}KB), max size is {1}KB".format(size / 1024, AVATAR_MAX_SIZE / 1024)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def hash_file(file):
|
||||
hasher = md5()
|
||||
buf = file.read()
|
||||
hasher.update(buf)
|
||||
file.seek(0)
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def save_avatar(file, user_id):
|
||||
if file and allowed_avatar_file(file):
|
||||
ext = file.mimetype.replace('image/', '')
|
||||
filename = "{0}.{1}.{2}".format(user_id, hash_file(file), ext)
|
||||
file.save(os.path.join(UPLOAD_DIRECTORY, filename))
|
||||
return filename
|
||||
|
||||
|
||||
def remove_avatar(url, user_id):
|
||||
match = re.search(r'/api/v1/users/avatar/(\d+.\w+.\w+)', url)
|
||||
if match:
|
||||
filename = match.group(1)
|
||||
if filename.startswith(str(user_id) + '.'):
|
||||
os.remove(os.path.join(UPLOAD_DIRECTORY, filename))
|
||||
|
||||
|
||||
def send_upload(filename):
|
||||
return send_from_directory(UPLOAD_DIRECTORY, secure_filename(filename))
|
|
@ -0,0 +1 @@
|
|||
from . import dev_contracts
|
|
@ -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)
|
|
@ -0,0 +1,145 @@
|
|||
import time
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_crowd_fund_abi():
|
||||
global crowd_fund_abi
|
||||
if crowd_fund_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 = '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)
|
||||
|
||||
crowd_fund = {}
|
||||
methods = [
|
||||
"immediateFirstMilestonePayout",
|
||||
"raiseGoal",
|
||||
"amountVotingForRefund",
|
||||
"beneficiary",
|
||||
"deadline",
|
||||
"milestoneVotingPeriod",
|
||||
"frozen",
|
||||
"isRaiseGoalReached",
|
||||
]
|
||||
|
||||
# batched
|
||||
calls = list(map(lambda x: [x, None], methods))
|
||||
try:
|
||||
crowd_fund = batch_call(current_web3, address, crowd_fund_abi, calls, contract)
|
||||
# catch dead contracts here
|
||||
except RpcError:
|
||||
return None
|
||||
|
||||
# balance (sync)
|
||||
crowd_fund['balance'] = current_web3.eth.getBalance(address)
|
||||
|
||||
# arrays (sync)
|
||||
crowd_fund['milestones'] = call_array(contract.functions.milestones)
|
||||
crowd_fund['trustees'] = call_array(contract.functions.trustees)
|
||||
contributor_list = call_array(contract.functions.contributorList)
|
||||
|
||||
# make milestones
|
||||
def make_ms(enum_ms):
|
||||
index = enum_ms[0]
|
||||
ms = enum_ms[1]
|
||||
is_immediate = index == 0 and crowd_fund['immediateFirstMilestonePayout']
|
||||
deadline = ms[2] * 1000
|
||||
amount_against = ms[1]
|
||||
pct_against = round(amount_against * 100 / crowd_fund['raiseGoal'])
|
||||
paid = ms[3]
|
||||
state = 'WAITING'
|
||||
if crowd_fund["isRaiseGoalReached"] and deadline > 0:
|
||||
if paid:
|
||||
state = 'PAID'
|
||||
elif deadline > time.time() * 1000:
|
||||
state = 'ACTIVE'
|
||||
elif pct_against >= 50:
|
||||
state = 'REJECTED'
|
||||
else:
|
||||
state = 'PAID'
|
||||
return {
|
||||
"index": index,
|
||||
"state": state,
|
||||
"amount": str(ms[0]),
|
||||
"amountAgainstPayout": str(amount_against),
|
||||
"percentAgainstPayout": pct_against,
|
||||
"payoutRequestVoteDeadline": deadline,
|
||||
"isPaid": paid,
|
||||
"isImmediatePayout": is_immediate
|
||||
}
|
||||
|
||||
crowd_fund['milestones'] = list(map(make_ms, enumerate(crowd_fund['milestones'])))
|
||||
|
||||
# contributor calls (batched)
|
||||
contributors_calls = list(map(lambda c_addr: ['contributors', (c_addr,)], contributor_list))
|
||||
contrib_votes_calls = []
|
||||
for c_addr in contributor_list:
|
||||
for msi in range(len(crowd_fund['milestones'])):
|
||||
contrib_votes_calls.append(['getContributorMilestoneVote', (c_addr, msi)])
|
||||
derived_calls = contributors_calls + contrib_votes_calls
|
||||
derived_results = batch_call(current_web3, address, crowd_fund_abi, derived_calls, contract)
|
||||
|
||||
# make contributors
|
||||
contributors = []
|
||||
for contrib_address in contributor_list:
|
||||
contrib_raw = derived_results['contributors' + contrib_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 = {
|
||||
"address": contrib_address,
|
||||
"contributionAmount": str(contrib_raw[0]),
|
||||
"refundVote": contrib_raw[1],
|
||||
"refunded": contrib_raw[2],
|
||||
"milestoneNoVotes": no_votes,
|
||||
}
|
||||
contributors.append(contrib)
|
||||
crowd_fund['contributors'] = contributors
|
||||
|
||||
# massage names and numbers
|
||||
crowd_fund['target'] = crowd_fund.pop('raiseGoal')
|
||||
crowd_fund['isFrozen'] = crowd_fund.pop('frozen')
|
||||
crowd_fund['deadline'] = crowd_fund['deadline'] * 1000
|
||||
crowd_fund['milestoneVotingPeriod'] = crowd_fund['milestoneVotingPeriod'] * 60 * 1000
|
||||
if crowd_fund['isRaiseGoalReached']:
|
||||
crowd_fund['funded'] = crowd_fund['target']
|
||||
crowd_fund['percentFunded'] = 100
|
||||
else:
|
||||
crowd_fund['funded'] = crowd_fund['balance']
|
||||
crowd_fund['percentFunded'] = round(crowd_fund['balance'] * 100 / crowd_fund['target'])
|
||||
crowd_fund['percentVotingForRefund'] = round(crowd_fund['amountVotingForRefund'] * 100 / crowd_fund['target'])
|
||||
|
||||
bn_keys = ['amountVotingForRefund', 'balance', 'funded', 'target']
|
||||
for k in bn_keys:
|
||||
crowd_fund[k] = str(crowd_fund[k])
|
||||
|
||||
return crowd_fund
|
||||
|
||||
|
||||
def validate_contribution_tx(tx_id, from_address, to_address, amount):
|
||||
amount_wei = current_web3.toWei(amount, 'ether')
|
||||
tx = current_web3.eth.getTransaction(tx_id)
|
||||
if tx:
|
||||
if from_address.lower() == tx.get("from").lower() and \
|
||||
to_address == tx.get("to") and \
|
||||
amount_wei == tx.get("value"):
|
||||
return True
|
||||
return False
|
|
@ -0,0 +1,86 @@
|
|||
import requests
|
||||
from web3.providers.base import JSONBaseProvider
|
||||
from web3.utils.contracts import prepare_transaction, find_matching_fn_abi
|
||||
from web3 import EthereumTesterProvider
|
||||
from grant.settings import ETHEREUM_ENDPOINT_URI
|
||||
from hexbytes import HexBytes
|
||||
from eth_abi import decode_abi
|
||||
from web3.utils.abi import get_abi_output_types, map_abi_data
|
||||
from web3.utils.normalizers import BASE_RETURN_NORMALIZERS
|
||||
|
||||
|
||||
class RpcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def call_array(fn):
|
||||
results = []
|
||||
no_error = True
|
||||
index = 0
|
||||
while no_error:
|
||||
try:
|
||||
results.append(fn(index).call())
|
||||
index += 1
|
||||
except Exception:
|
||||
no_error = False
|
||||
return results
|
||||
|
||||
|
||||
def make_key(method, args):
|
||||
return method + "".join(list(map(lambda z: str(z), args))) if args else method
|
||||
|
||||
|
||||
def tester_batch(calls, contract):
|
||||
# fallback to sync calls for eth-tester instead of implementing batching
|
||||
results = {}
|
||||
for call in calls:
|
||||
method, args = call
|
||||
args = args if args else ()
|
||||
results[make_key(method, args)] = contract.functions[method](*args).call()
|
||||
return results
|
||||
|
||||
|
||||
def batch(node_address, params):
|
||||
base_provider = JSONBaseProvider()
|
||||
request_data = b'[' + b','.join(
|
||||
[base_provider.encode_rpc_request('eth_call', p) for p in params]
|
||||
) + b']'
|
||||
r = requests.post(node_address, data=request_data, headers={'Content-Type': 'application/json'})
|
||||
responses = base_provider.decode_rpc_response(r.content)
|
||||
return responses
|
||||
|
||||
|
||||
def batch_call(w3, address, abi, calls, contract):
|
||||
# TODO: use web3py batching once its added
|
||||
# this implements batched rpc calls using web3py helper methods
|
||||
# web3py doesn't support this out-of-box yet
|
||||
# issue: https://github.com/ethereum/web3.py/issues/832
|
||||
if not calls:
|
||||
return []
|
||||
if type(w3.providers[0]) is EthereumTesterProvider:
|
||||
return tester_batch(calls, contract)
|
||||
inputs = []
|
||||
for c in calls:
|
||||
name, args = c
|
||||
tx = {"from": w3.eth.defaultAccount, "to": address}
|
||||
prepared = prepare_transaction(address, w3, name, abi, None, tx, args)
|
||||
inputs.append([prepared, 'latest'])
|
||||
responses = batch(ETHEREUM_ENDPOINT_URI, inputs)
|
||||
if 'error' in responses[0]:
|
||||
message = responses[0]['error']['message'] if 'message' in responses[0]['error'] else 'No error message found.'
|
||||
raise RpcError("rpc error: {0}".format(message))
|
||||
results = {}
|
||||
for r in zip(calls, responses):
|
||||
result = HexBytes(r[1]['result'])
|
||||
fn_id, args = r[0]
|
||||
fn_abi = find_matching_fn_abi(abi, fn_id, args)
|
||||
output_types = get_abi_output_types(fn_abi)
|
||||
output_data = decode_abi(output_types, result)
|
||||
normalized_data = map_abi_data(BASE_RETURN_NORMALIZERS, output_types, output_data)
|
||||
key = make_key(fn_id, args)
|
||||
if len(normalized_data) == 1:
|
||||
results[key] = normalized_data[0]
|
||||
else:
|
||||
results[key] = normalized_data
|
||||
|
||||
return results
|
|
@ -0,0 +1,38 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 1d06a5e43324
|
||||
Revises: 312db8611967
|
||||
Create Date: 2018-11-17 11:07:40.413141
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1d06a5e43324'
|
||||
down_revision = '312db8611967'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('proposal_contribution',
|
||||
sa.Column('tx_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('date_created', sa.DateTime(), nullable=False),
|
||||
sa.Column('proposal_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('from_address', sa.String(length=255), nullable=False),
|
||||
sa.Column('amount', sa.String(length=255), nullable=False),
|
||||
sa.ForeignKeyConstraint(['proposal_id'], ['proposal.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('tx_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('proposal_contribution')
|
||||
# ### end Alembic commands ###
|
|
@ -1,7 +1,7 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 3699cb98fc2a
|
||||
Revises: 312db8611967
|
||||
Revises: 1d06a5e43324
|
||||
Create Date: 2018-11-08 12:33:14.995080
|
||||
|
||||
"""
|
||||
|
@ -11,7 +11,7 @@ import sqlalchemy as sa
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3699cb98fc2a'
|
||||
down_revision = '312db8611967'
|
||||
down_revision = '1d06a5e43324'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
pytest==3.7.1
|
||||
WebTest==2.0.30
|
||||
factory-boy==2.11.1
|
||||
eth-tester[py-evm]==0.1.0b33
|
||||
|
||||
# Lint and code style
|
||||
flake8==3.5.0
|
||||
|
|
|
@ -55,4 +55,11 @@ flask-sendgrid==0.6
|
|||
sendgrid==5.3.0
|
||||
|
||||
# input validation
|
||||
flask-yolo2API==0.2.4
|
||||
flask-yolo2API==0.2.6
|
||||
|
||||
#web3
|
||||
flask-web3==0.1.1
|
||||
web3==4.8.1
|
||||
|
||||
#sentry
|
||||
sentry-sdk[flask]==0.5.5
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from mock import patch
|
||||
|
||||
from grant.proposal.models import Proposal
|
||||
from grant.user.models import SocialMedia, Avatar
|
||||
|
@ -71,3 +72,110 @@ class TestAPI(BaseUserConfig):
|
|||
)
|
||||
|
||||
self.assertEqual(proposal_res2.status_code, 409)
|
||||
|
||||
@patch('grant.proposal.views.validate_contribution_tx', return_value=True)
|
||||
def test_create_proposal_contribution(self, mock_validate_contribution_tx):
|
||||
proposal_res = self.app.post(
|
||||
"/api/v1/proposals/",
|
||||
data=json.dumps(test_proposal),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
proposal_json = proposal_res.json
|
||||
proposal_id = proposal_json["proposalId"]
|
||||
|
||||
contribution = {
|
||||
"txId": "0x12345",
|
||||
"fromAddress": "0x23456",
|
||||
"amount": "1.2345"
|
||||
}
|
||||
|
||||
contribution_res = self.app.post(
|
||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
||||
data=json.dumps(contribution),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
res = contribution_res.json
|
||||
exp = contribution
|
||||
|
||||
def eq(k):
|
||||
self.assertEqual(exp[k], res[k])
|
||||
eq("txId")
|
||||
eq("fromAddress")
|
||||
eq("amount")
|
||||
self.assertEqual(proposal_id, res["proposalId"])
|
||||
|
||||
@patch('grant.proposal.views.validate_contribution_tx', return_value=True)
|
||||
def test_get_proposal_contribution(self, mock_validate_contribution_tx):
|
||||
proposal_res = self.app.post(
|
||||
"/api/v1/proposals/",
|
||||
data=json.dumps(test_proposal),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
proposal_json = proposal_res.json
|
||||
proposal_id = proposal_json["proposalId"]
|
||||
|
||||
contribution = {
|
||||
"txId": "0x12345",
|
||||
"fromAddress": "0x23456",
|
||||
"amount": "1.2345"
|
||||
}
|
||||
|
||||
self.app.post(
|
||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
||||
data=json.dumps(contribution),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
contribution_res = self.app.get(
|
||||
"/api/v1/proposals/{0}/contributions/{1}".format(proposal_id, contribution["txId"])
|
||||
)
|
||||
res = contribution_res.json
|
||||
exp = contribution
|
||||
|
||||
def eq(k):
|
||||
self.assertEqual(exp[k], res[k])
|
||||
eq("txId")
|
||||
eq("fromAddress")
|
||||
eq("amount")
|
||||
self.assertEqual(proposal_id, res["proposalId"])
|
||||
|
||||
@patch('grant.proposal.views.validate_contribution_tx', return_value=True)
|
||||
def test_get_proposal_contributions(self, mock_validate_contribution_tx):
|
||||
proposal_res = self.app.post(
|
||||
"/api/v1/proposals/",
|
||||
data=json.dumps(test_proposal),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
proposal_json = proposal_res.json
|
||||
proposal_id = proposal_json["proposalId"]
|
||||
|
||||
contribution = {
|
||||
"txId": "0x12345",
|
||||
"fromAddress": "0x23456",
|
||||
"amount": "1.2345"
|
||||
}
|
||||
|
||||
self.app.post(
|
||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
||||
data=json.dumps(contribution),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
contributions_res = self.app.get(
|
||||
"/api/v1/proposals/{0}/contributions".format(proposal_id)
|
||||
)
|
||||
res = contributions_res.json[0]
|
||||
exp = contribution
|
||||
|
||||
def eq(k):
|
||||
self.assertEqual(exp[k], res[k])
|
||||
eq("txId")
|
||||
eq("fromAddress")
|
||||
eq("amount")
|
||||
self.assertEqual(proposal_id, res["proposalId"])
|
||||
|
|
|
@ -8,3 +8,5 @@ DEBUG_TB_ENABLED = False
|
|||
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
WTF_CSRF_ENABLED = False # Allows form testing
|
||||
ETHEREUM_PROVIDER = "test"
|
||||
ETHEREUM_ENDPOINT_URI = ""
|
||||
|
|
|
@ -2,10 +2,10 @@ import copy
|
|||
import json
|
||||
|
||||
from animal_case import animalify
|
||||
from mock import patch
|
||||
|
||||
from grant.proposal.models import Proposal
|
||||
from grant.user.models import User, user_schema
|
||||
from mock import patch
|
||||
|
||||
from ..config import BaseUserConfig
|
||||
from ..test_data import test_team, test_proposal, test_user
|
||||
|
||||
|
@ -195,8 +195,8 @@ class TestAPI(BaseUserConfig):
|
|||
def test_update_user_remove_social_and_avatar(self):
|
||||
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
|
||||
updated_user["displayName"] = 'new display name'
|
||||
updated_user["avatar"] = None
|
||||
updated_user["socialMedias"] = None
|
||||
updated_user["avatar"] = {}
|
||||
updated_user["socialMedias"] = []
|
||||
|
||||
user_update_resp = self.app.put(
|
||||
"/api/v1/users/{}".format(self.user.account_address),
|
||||
|
@ -211,3 +211,15 @@ class TestAPI(BaseUserConfig):
|
|||
self.assertFalse(len(user_json["socialMedias"]))
|
||||
self.assertEqual(user_json["displayName"], updated_user["displayName"])
|
||||
self.assertEqual(user_json["title"], updated_user["title"])
|
||||
|
||||
def test_update_user_400_when_required_param_not_passed(self):
|
||||
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
|
||||
updated_user["displayName"] = 'new display name'
|
||||
del updated_user["avatar"]
|
||||
user_update_resp = self.app.put(
|
||||
"/api/v1/users/{}".format(self.user.account_address),
|
||||
data=json.dumps(updated_user),
|
||||
headers=self.headers,
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assert400(user_update_resp)
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
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
|
||||
|
||||
# increase gas limit on eth-tester
|
||||
# https://github.com/ethereum/web3.py/issues/1013
|
||||
# https://gitter.im/ethereum/py-evm?at=5b7eb68c4be56c5918854337
|
||||
py_evm_main.GENESIS_GAS_LIMIT = 10000000
|
||||
|
||||
|
||||
class TestWeb3ProposalRead(BaseTestConfig):
|
||||
def create_app(self):
|
||||
self.real_app = BaseTestConfig.create_app(self)
|
||||
return self.real_app
|
||||
|
||||
def setUp(self):
|
||||
BaseTestConfig.setUp(self)
|
||||
# the following will properly configure web3 with test config
|
||||
web3.init_app(self.real_app)
|
||||
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'])
|
||||
tx_hash = CrowdFundFactory.constructor().transact()
|
||||
tx_receipt = current_web3.eth.waitForTransactionReceipt(tx_hash)
|
||||
self.crowd_fund_factory = current_web3.eth.contract(
|
||||
address=tx_receipt.contractAddress,
|
||||
abi=crowd_fund_factory_json['abi']
|
||||
)
|
||||
|
||||
def get_mock_proposal_read(self, contributors=[]):
|
||||
mock_proposal_read = {
|
||||
"immediateFirstMilestonePayout": True,
|
||||
"amountVotingForRefund": "0",
|
||||
"beneficiary": current_web3.eth.accounts[0],
|
||||
# "deadline": 1541706179000,
|
||||
"milestoneVotingPeriod": 3600000,
|
||||
"isRaiseGoalReached": False,
|
||||
"balance": "0",
|
||||
"milestones": [
|
||||
{
|
||||
"index": 0,
|
||||
"state": "WAITING",
|
||||
"amount": "5000000000000000000",
|
||||
"amountAgainstPayout": "0",
|
||||
"percentAgainstPayout": 0,
|
||||
"payoutRequestVoteDeadline": 0,
|
||||
"isPaid": False,
|
||||
"isImmediatePayout": True
|
||||
}
|
||||
],
|
||||
"trustees": [current_web3.eth.accounts[0]],
|
||||
"contributors": [],
|
||||
"target": "5000000000000000000",
|
||||
"isFrozen": False,
|
||||
"funded": "0",
|
||||
"percentFunded": 0,
|
||||
"percentVotingForRefund": 0
|
||||
}
|
||||
for c in contributors:
|
||||
mock_proposal_read['contributors'].append({
|
||||
"address": current_web3.eth.accounts[c[0]],
|
||||
"contributionAmount": str(c[1] * 1000000000000000000),
|
||||
"refundVote": False,
|
||||
"refunded": False,
|
||||
"milestoneNoVotes": [False]
|
||||
})
|
||||
return mock_proposal_read
|
||||
|
||||
def create_crowd_fund(self):
|
||||
tx_hash = self.crowd_fund_factory.functions.createCrowdFund(
|
||||
5000000000000000000, # ethAmount
|
||||
current_web3.eth.accounts[0], # payout
|
||||
[current_web3.eth.accounts[0]], # trustees
|
||||
[5000000000000000000], # milestone amounts
|
||||
60, # duration (minutes)
|
||||
60, # voting period (minutes)
|
||||
True # immediate first milestone payout
|
||||
).transact()
|
||||
tx_receipt = current_web3.eth.waitForTransactionReceipt(tx_hash)
|
||||
tx_events = self.crowd_fund_factory.events.ContractCreated().processReceipt(tx_receipt)
|
||||
contract_address = tx_events[0]['args']['newAddress']
|
||||
return contract_address
|
||||
|
||||
def fund_crowd_fund(self, address):
|
||||
contract = current_web3.eth.contract(address=address, abi=self.crowd_fund_json['abi'])
|
||||
accts = current_web3.eth.accounts
|
||||
for c in [[5, 1], [6, 1], [7, 3]]:
|
||||
tx_hash = contract.functions.contribute().transact({
|
||||
"from": accts[c[0]],
|
||||
"value": c[1] * 1000000000000000000
|
||||
})
|
||||
current_web3.eth.waitForTransactionReceipt(tx_hash)
|
||||
|
||||
def test_proposal_read_new(self):
|
||||
contract_address = self.create_crowd_fund()
|
||||
proposal_read = read_proposal(contract_address)
|
||||
deadline = proposal_read.pop('deadline')
|
||||
deadline_diff = deadline - time.time() * 1000
|
||||
self.assertGreater(60000, deadline_diff)
|
||||
self.assertGreater(deadline_diff, 50000)
|
||||
self.maxDiff = None
|
||||
self.assertEqual(proposal_read, self.get_mock_proposal_read())
|
||||
|
||||
def test_proposal_funded(self):
|
||||
contract_address = self.create_crowd_fund()
|
||||
self.fund_crowd_fund(contract_address)
|
||||
proposal_read = read_proposal(contract_address)
|
||||
expected = self.get_mock_proposal_read([[5, 1], [6, 1], [7, 3]])
|
||||
expected['funded'] = expected['target']
|
||||
expected['balance'] = expected['target']
|
||||
expected['isRaiseGoalReached'] = True
|
||||
expected['percentFunded'] = 100
|
||||
deadline = proposal_read.pop('deadline')
|
||||
deadline_diff = deadline - time.time() * 1000
|
||||
self.assertGreater(60000, deadline_diff)
|
||||
self.assertGreater(deadline_diff, 50000)
|
||||
self.maxDiff = None
|
||||
self.assertEqual(proposal_read, expected)
|
|
@ -2,5 +2,4 @@ node_modules
|
|||
.idea/
|
||||
yarn-error.log
|
||||
.env
|
||||
build/abi
|
||||
build/typedefs
|
||||
build
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
8.13.0
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1,3 @@
|
|||
{}
|
||||
{
|
||||
"baseUrl": "http://localhost:3000"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,14 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
increaseTime,
|
||||
syncTimeWithEvm,
|
||||
randomString
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("authenticate", () => {
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
});
|
|
@ -3,13 +3,13 @@ import { loadWeb3, randomString, randomHex } from "../helpers";
|
|||
|
||||
describe("browse", () => {
|
||||
it("should load and be able to browse pages", () => {
|
||||
cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3 });
|
||||
cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(0) });
|
||||
cy.title().should("include", "Grant.io - Home");
|
||||
|
||||
// test hero create link
|
||||
cy.get('.Home-hero-buttons a[href="/create"]')
|
||||
// {force: true} here overcomes a strange issue where the button moves up under the header
|
||||
// this is likely a cypress-related problem
|
||||
// this is likely a cypress scroll related problem
|
||||
.click({ force: true });
|
||||
cy.title().should("include", "Grant.io - Create a Proposal");
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
increaseTime,
|
||||
syncTimeWithEvm,
|
||||
randomString
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("proposal", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e create cancel`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
(Cypress as any).runner.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("cancels the proposal", () => {
|
||||
cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click();
|
||||
cy.contains(".ant-dropdown-menu-item", "Cancel proposal").click();
|
||||
cy.contains(".ant-modal-footer > div button", "Confirm").click();
|
||||
cy.contains("body", "Proposal didn’t get funded", { timeout: 20000 });
|
||||
cy.get(".ant-modal-wrap").should("not.be.visible");
|
||||
cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click();
|
||||
cy.contains(".ant-dropdown-menu-item", "Cancel proposal").should(
|
||||
"have.attr",
|
||||
"aria-disabled",
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
it("should appear unfunded to outsiders (account 9)", () => {
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(9) }));
|
||||
cy.contains("body", "Proposal didn’t get funded", { timeout: 20000 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,173 @@
|
|||
/// <reference types="cypress"/>
|
||||
import { loadWeb3, randomString, randomHex, syncTimeWithEvm } from "../helpers";
|
||||
import { authenticateUser } from "../parts";
|
||||
|
||||
describe("create.flow", () => {
|
||||
const time = new Date().toLocaleString();
|
||||
const id = randomString();
|
||||
const randomEthHex = randomHex(32);
|
||||
const nextYear = new Date().getUTCFullYear() + 1;
|
||||
const proposal = {
|
||||
title: `[${id}] e2e create flow`,
|
||||
brief: "e2e brief",
|
||||
category: "Community", // .anticon-team
|
||||
targetAmount: 5,
|
||||
body: `#### e2e Proposal ${id} {enter} **created** ${time} `,
|
||||
team: [
|
||||
{
|
||||
name: "Alisha Endtoend",
|
||||
title: "QA Robot0",
|
||||
ethAddress: `0x0000${randomEthHex}0000`,
|
||||
emailAddress: `qa.alisha.${id}@grant.io`
|
||||
},
|
||||
{
|
||||
name: "Billy Endtoend",
|
||||
title: "QA Robot1",
|
||||
ethAddress: `0x1111${randomEthHex}1111`,
|
||||
emailAddress: `qa.billy.${id}@grant.io`
|
||||
}
|
||||
],
|
||||
milestones: [
|
||||
{
|
||||
title: `e2e Milestone ${id} 0`,
|
||||
body: `e2e Milestone ${id} {enter} body 0`,
|
||||
date: {
|
||||
y: nextYear,
|
||||
m: "Jan",
|
||||
expect: "January " + nextYear
|
||||
}
|
||||
},
|
||||
{
|
||||
title: `e2e Milestone ${id} 1`,
|
||||
body: `e2e Milestone ${id} {enter} body 1`,
|
||||
date: {
|
||||
y: nextYear,
|
||||
m: "Feb",
|
||||
expect: "February " + nextYear
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
(Cypress as any).runner.stop();
|
||||
}
|
||||
});
|
||||
|
||||
context("create flow wizard", () => {
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("create flow step 1", () => {
|
||||
cy.get('[href="/create"]').click();
|
||||
syncTimeWithEvm(cy);
|
||||
cy.get('.CreateFlow input[name="title"]').type(proposal.title);
|
||||
cy.get('.CreateFlow textarea[name="brief"]').type(proposal.brief);
|
||||
cy.contains("Select a category").click();
|
||||
cy.get(".ant-select-dropdown li .anticon-team").click();
|
||||
cy.get('.CreateFlow input[name="amountToRaise"]').type(
|
||||
"" + proposal.targetAmount
|
||||
);
|
||||
cy.wait(1000);
|
||||
cy.contains(".CreateFlow-footer-button", "Continue").click();
|
||||
});
|
||||
|
||||
it("create flow step 2", () => {
|
||||
cy.get("button.TeamForm-add").click();
|
||||
cy.get('.TeamMember-info input[name="name"]').type(proposal.team[1].name);
|
||||
cy.get('.TeamMember-info input[name="title"]').type(
|
||||
proposal.team[1].title
|
||||
);
|
||||
cy.get('.TeamMember-info input[name="ethAddress"]').type(
|
||||
proposal.team[1].ethAddress
|
||||
);
|
||||
cy.get('.TeamMember-info input[name="emailAddress"]').type(
|
||||
proposal.team[1].emailAddress
|
||||
);
|
||||
cy.get("button")
|
||||
.contains("Save changes")
|
||||
.click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.contains(".CreateFlow-footer-button", "Continue").click();
|
||||
});
|
||||
|
||||
it("create flow step 3", () => {
|
||||
cy.get(".DraftEditor-editorContainer > div").type(proposal.body);
|
||||
cy.get(".mde-tabs > :nth-child(2)").click();
|
||||
cy.wait(1000);
|
||||
cy.contains(".CreateFlow-footer-button", "Continue").click();
|
||||
});
|
||||
|
||||
it("create flow step 4", () => {
|
||||
cy.get('input[name="title"]').type(proposal.milestones[0].title);
|
||||
cy.get('textarea[name="body"]').type(proposal.milestones[0].body);
|
||||
cy.get('input[placeholder="Expected completion date"]').click();
|
||||
cy.get(".ant-calendar-month-panel-next-year-btn").click();
|
||||
cy.get(".ant-calendar-month-panel-month")
|
||||
.contains(proposal.milestones[0].date.m)
|
||||
.click();
|
||||
cy.get(".ant-calendar-picker-input").should(
|
||||
"have.value",
|
||||
proposal.milestones[0].date.expect
|
||||
);
|
||||
cy.get("button")
|
||||
.contains("Add another milestone")
|
||||
.click({ force: true });
|
||||
cy.get('input[name="title"]')
|
||||
.eq(1)
|
||||
.type(proposal.milestones[1].title);
|
||||
cy.get('textarea[name="body"]')
|
||||
.eq(1)
|
||||
.type(proposal.milestones[1].body);
|
||||
cy.get('input[placeholder="Expected completion date"]')
|
||||
.eq(1)
|
||||
.click();
|
||||
cy.get(".ant-calendar-month-panel-next-year-btn").click();
|
||||
cy.get(".ant-calendar-month-panel-month")
|
||||
.contains(proposal.milestones[1].date.m)
|
||||
.click();
|
||||
cy.get(".ant-calendar-picker-input")
|
||||
.eq(1)
|
||||
.should("have.value", proposal.milestones[1].date.expect);
|
||||
cy.wait(1000);
|
||||
cy.contains(".CreateFlow-footer-button", "Continue").click();
|
||||
});
|
||||
|
||||
it("create flow step 5", () => {
|
||||
cy.window()
|
||||
.then(w => (w as any).web3.eth.getAccounts())
|
||||
.then(accts => {
|
||||
cy.get('input[name="payOutAddress"]').type(accts[0]);
|
||||
cy.get("button")
|
||||
.contains("Add another trustee")
|
||||
.click({ force: true });
|
||||
cy.get(
|
||||
'input[placeholder="0x8B0B72F8bDE212991135668922fD5acE557DE6aB"]'
|
||||
)
|
||||
.eq(1)
|
||||
.type(accts[1]);
|
||||
cy.get('input[name="deadline"][value="2592000"]').click({
|
||||
force: true
|
||||
});
|
||||
cy.get('input[name="milestoneDeadline"][value="259200"]').click({
|
||||
force: true
|
||||
});
|
||||
});
|
||||
cy.wait(1000);
|
||||
cy.contains(".CreateFlow-footer-button", "Continue").click();
|
||||
});
|
||||
|
||||
it("publishes the proposal", () => {
|
||||
cy.get("button")
|
||||
.contains("Publish")
|
||||
.click();
|
||||
cy.get(".CreateFinal-loader-text").contains("Deploying contract...");
|
||||
cy.get(".CreateFinal-message-text a", { timeout: 20000 })
|
||||
.contains("Click here")
|
||||
.click();
|
||||
cy.get(".Proposal-top-main-title").contains(proposal.title);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
increaseTime,
|
||||
syncTimeWithEvm,
|
||||
randomString
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.cancel", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e create fund cancel`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
(Cypress as any).runner.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("create demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("funds the proposal with account 5", () => {
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.get(".ant-input", { timeout: 20000 }).type(amount);
|
||||
cy.get(".ant-form > .ant-btn").click();
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels the proposal (refund contributors)", () => {
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click();
|
||||
cy.contains(".ant-dropdown-menu-item", "Refund contributors").click();
|
||||
cy.contains(".ant-modal-footer > div button", "Confirm").click();
|
||||
cy.get(".ant-modal-wrap", { timeout: 20000 }).should("not.be.visible");
|
||||
cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click();
|
||||
cy.contains(".ant-dropdown-menu-item", "Refund contributors").should(
|
||||
"have.attr",
|
||||
"aria-disabled",
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
it("refunds the contributor (account 5)", () => {
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-tabs-nav > :nth-child(1) > :nth-child(4)", "Refund", {
|
||||
timeout: 20000
|
||||
}).click();
|
||||
// force disables cypress' auto scrolling which messes up UI in this case
|
||||
cy.contains(".ant-btn", "Get your refund").click({ force: true });
|
||||
cy.contains("body", "Your refund has been processed", { timeout: 20000 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
randomString,
|
||||
syncTimeWithEvm,
|
||||
increaseTime
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, fundProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.complete.minority-no-votes", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e minority no-votes complete`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
//(Cypress as any).runner.stop();
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("funds the proposal from accounts 5, 6 & 7", () => {
|
||||
fundProposal(cy, 5, 0.1);
|
||||
fundProposal(cy, 6, 0.2);
|
||||
fundProposal(cy, 7, 0.7);
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout", () => {
|
||||
// MILESTONE 1
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("requests milestone 2 payout", () => {
|
||||
// MILESTONE 2
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
|
||||
it("minority funder (acct 5) votes no", () => {
|
||||
// VOTE NO
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 })
|
||||
.click()
|
||||
.should("have.class", "ant-btn-loading");
|
||||
cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 });
|
||||
});
|
||||
|
||||
it("expires milestone 2 voting period & receives payout", () => {
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
// RECEIVE PAYOUT
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("requests milestone 3 payout", () => {
|
||||
// MILESTONE 3
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
|
||||
it("minority funder (acct 5) votes no", () => {
|
||||
// VOTE NO
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 })
|
||||
.click()
|
||||
.should("have.class", "ant-btn-loading");
|
||||
cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 });
|
||||
});
|
||||
|
||||
it("expires milestone 3 voting period & receives payout", () => {
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
// RECEIVE PAYOUT
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("should not have receive button", () => {
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).should("not.exist");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
increaseTime,
|
||||
syncTimeWithEvm,
|
||||
randomString
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.complete", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e create fund complete`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
//(Cypress as any).runner.stop();
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("create demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("funds the proposal with account 5", () => {
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.get(".ant-input", { timeout: 20000 }).type(amount);
|
||||
cy.get(".ant-form > .ant-btn").click();
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout", () => {
|
||||
// MILESTONE 1
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request initial payout",
|
||||
{ timeout: 20000 }
|
||||
)
|
||||
.as("RequestPayout")
|
||||
.click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("requests and receives milestone 2 payout", () => {
|
||||
// MILESTONE 2
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("requests and receives milestone 3 payout", () => {
|
||||
// MILESTONE 3
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("should not have receive button", () => {
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).should("not.exist");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/// <reference types="cypress"/>
|
||||
import { loadWeb3, randomString, syncTimeWithEvm } from "../helpers";
|
||||
import { createDemoProposal, fundProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.ms2.majority-no-vote", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e ms2 majority no-vote`;
|
||||
const amount = "1";
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("fund the proposal with 5th account", () => {
|
||||
fundProposal(cy, 5, 1);
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout for milestone 1", () => {
|
||||
// MILESTONE 1
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("requests milestone 2 payout", () => {
|
||||
// MILESTONE 2
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
|
||||
it("vote against milestone 2 payout as account 5", () => {
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }).click();
|
||||
cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
randomString,
|
||||
syncTimeWithEvm,
|
||||
increaseTime
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, fundProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.ms2.no-vote.re-vote", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e ms2 no-vote expire re-vote`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
//(Cypress as any).runner.stop();
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("fund the proposal with 5th account", () => {
|
||||
fundProposal(cy, 5, 1);
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout for milestone 1", () => {
|
||||
// MILESTONE 1
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("request milestone 2 payout", () => {
|
||||
// MILESTONE 2
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
|
||||
it("vote against milestone 2 payout as 5th account", () => {
|
||||
// reload page with 5th account
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 })
|
||||
.click()
|
||||
.should("have.class", "ant-btn-loading");
|
||||
cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 });
|
||||
});
|
||||
|
||||
it("milestone 2 vote expires and payout is requested again", () => {
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains("Payout was voted against");
|
||||
// RE-REQUEST PAYOUT
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
// TODO: fix this bug (the following fails)
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
randomString,
|
||||
syncTimeWithEvm,
|
||||
increaseTime
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, fundProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.ms2.refund-after-payout", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e ms2 refund after payout`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
//(Cypress as any).runner.stop();
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("fund the proposal with account 5", () => {
|
||||
fundProposal(cy, 5, 1);
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout for milestone 1", () => {
|
||||
// MILESTONE 1
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("majority refund vote and get refund (account 5)", () => {
|
||||
// REFUND
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-tabs-nav > :nth-child(1) > :nth-child(4)", "Refund", {
|
||||
timeout: 20000
|
||||
}).click();
|
||||
// INCREASE TIME
|
||||
increaseTime(cy, 70000);
|
||||
// force disables cypress' auto scrolling which messes up UI in this case
|
||||
cy.contains(".ant-btn", "Vote for refund").click({ force: true });
|
||||
cy.contains(".ant-btn", "Get your refund", { timeout: 20000 }).click({
|
||||
force: true
|
||||
});
|
||||
cy.contains("body", "Your refund has been processed", { timeout: 20000 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
randomString,
|
||||
syncTimeWithEvm,
|
||||
increaseTime
|
||||
} from "../helpers";
|
||||
import { createDemoProposal, fundProposal, authenticateUser } from "../parts";
|
||||
|
||||
describe("create.fund.ms2.revert-no-vote", () => {
|
||||
const id = randomString();
|
||||
const title = `[${id}] e2e ms2 revert no-vote`;
|
||||
const amount = "1";
|
||||
|
||||
afterEach(function() {
|
||||
if (this.currentTest.state === "failed") {
|
||||
//(Cypress as any).runner.stop();
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates and creates if necessary", () => {
|
||||
authenticateUser(cy, 0);
|
||||
});
|
||||
|
||||
it("creates demo proposal", () => {
|
||||
createDemoProposal(cy, title, amount);
|
||||
});
|
||||
|
||||
it("funds the proposal with 5th account", () => {
|
||||
fundProposal(cy, 5, 1);
|
||||
cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains(
|
||||
"Proposal has been funded"
|
||||
);
|
||||
});
|
||||
|
||||
it("receives initial payout for milestone 1", () => {
|
||||
// MILESTONE 1
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click();
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive initial payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
});
|
||||
|
||||
it("requests milestone 2 payout", () => {
|
||||
// MILESTONE 2
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
});
|
||||
|
||||
it("votes against milestone 2 payout as account 5 and then reverts the vote", () => {
|
||||
// NO VOTE... REVERT
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) }));
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 })
|
||||
.click()
|
||||
.should("have.class", "ant-btn-loading");
|
||||
cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 })
|
||||
.click()
|
||||
.should("have.class", "ant-btn-loading");
|
||||
cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 });
|
||||
});
|
||||
|
||||
it("milestone 2 vote expires and payout is received", () => {
|
||||
// EXPIRE
|
||||
increaseTime(cy, 70000);
|
||||
// PAYOUT
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) }));
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Receive milestone payout"
|
||||
).click();
|
||||
});
|
||||
|
||||
it("milestone 3 becomes active", () => {
|
||||
// MILESTONE 3
|
||||
cy.contains(
|
||||
".MilestoneAction-top > div > .ant-btn",
|
||||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,192 +0,0 @@
|
|||
/// <reference types="cypress"/>
|
||||
import { loadWeb3, randomString, randomHex } from "../helpers";
|
||||
|
||||
describe("create proposal", () => {
|
||||
it("should load and be able to browse pages", () => {
|
||||
cy.visit("http://localhost:3000/create", { onBeforeLoad: loadWeb3 });
|
||||
|
||||
// populate ethAccounts
|
||||
cy.wait(1000);
|
||||
cy.window()
|
||||
.then(w => (w as any).web3.eth.getAccounts())
|
||||
.as("EthAccounts");
|
||||
|
||||
// demo proposal
|
||||
// cy.get("button.CreateFlow-footer-example").click();
|
||||
|
||||
const time = new Date().toLocaleString();
|
||||
const id = randomString();
|
||||
const randomEthHex = randomHex(32);
|
||||
const nextYear = new Date().getUTCFullYear() + 1;
|
||||
const proposal = {
|
||||
title: "e2e - smoke - create " + time,
|
||||
brief: "e2e brief",
|
||||
category: "Community", // .anticon-team
|
||||
targetAmount: 5,
|
||||
body: `#### e2e Proposal ${id} {enter} **created** ${time} `,
|
||||
team: [
|
||||
{
|
||||
name: "Alisha Endtoend",
|
||||
title: "QA Robot0",
|
||||
ethAddress: `0x0000${randomEthHex}0000`,
|
||||
emailAddress: `qa.alisha.${id}@grant.io`
|
||||
},
|
||||
{
|
||||
name: "Billy Endtoend",
|
||||
title: "QA Robot1",
|
||||
ethAddress: `0x1111${randomEthHex}1111`,
|
||||
emailAddress: `qa.billy.${id}@grant.io`
|
||||
}
|
||||
],
|
||||
milestones: [
|
||||
{
|
||||
title: `e2e Milestone ${id} 0`,
|
||||
body: `e2e Milestone ${id} {enter} body 0`,
|
||||
date: {
|
||||
y: nextYear,
|
||||
m: "Jan",
|
||||
expect: "January " + nextYear
|
||||
}
|
||||
},
|
||||
{
|
||||
title: `e2e Milestone ${id} 1`,
|
||||
body: `e2e Milestone ${id} {enter} body 1`,
|
||||
date: {
|
||||
y: nextYear,
|
||||
m: "Feb",
|
||||
expect: "February " + nextYear
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// step 1
|
||||
cy.get('.CreateFlow input[name="title"]', { timeout: 20000 }).type(
|
||||
proposal.title
|
||||
);
|
||||
cy.get('.CreateFlow textarea[name="brief"]').type(proposal.brief);
|
||||
cy.contains("Select a category").click();
|
||||
cy.get(".ant-select-dropdown li .anticon-team").click();
|
||||
cy.get('.CreateFlow input[name="amountToRaise"]').type(
|
||||
"" + proposal.targetAmount
|
||||
);
|
||||
cy.get(".CreateFlow-footer-button")
|
||||
.contains("Continue")
|
||||
.as("Continue")
|
||||
.click();
|
||||
|
||||
// step 2
|
||||
cy.get('.TeamMember-info input[name="name"]').type(proposal.team[0].name);
|
||||
cy.get('.TeamMember-info input[name="title"]').type(proposal.team[0].title);
|
||||
cy.get("@EthAccounts").then(accts => {
|
||||
cy.get('.TeamMember-info input[name="ethAddress"]').type(
|
||||
accts[0].toString()
|
||||
);
|
||||
});
|
||||
cy.get('.TeamMember-info input[name="emailAddress"]').type(
|
||||
proposal.team[0].emailAddress
|
||||
);
|
||||
cy.get("button")
|
||||
.contains("Save changes")
|
||||
.click({ force: true });
|
||||
|
||||
cy.get("button.TeamForm-add").click();
|
||||
cy.get('.TeamMember-info input[name="name"]').type(proposal.team[1].name);
|
||||
cy.get('.TeamMember-info input[name="title"]').type(proposal.team[1].title);
|
||||
cy.get("@EthAccounts").then(accts => {
|
||||
cy.get('.TeamMember-info input[name="ethAddress"]').type(
|
||||
accts[1].toString()
|
||||
);
|
||||
});
|
||||
cy.get('.TeamMember-info input[name="emailAddress"]').type(
|
||||
proposal.team[1].emailAddress
|
||||
);
|
||||
cy.get("button")
|
||||
.contains("Save changes")
|
||||
.click({ force: true });
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get("@Continue").click();
|
||||
|
||||
// step 3
|
||||
cy.get(".DraftEditor-editorContainer > div").type(proposal.body);
|
||||
cy.get(".mde-tabs > :nth-child(2)").click();
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get("@Continue").click();
|
||||
|
||||
// step 4
|
||||
cy.get('input[name="title"]').type(proposal.milestones[0].title);
|
||||
cy.get('textarea[name="body"]').type(proposal.milestones[0].body);
|
||||
cy.get('input[placeholder="Expected completion date"]').click();
|
||||
cy.get(".ant-calendar-month-panel-next-year-btn").click();
|
||||
cy.get(".ant-calendar-month-panel-month")
|
||||
.contains(proposal.milestones[0].date.m)
|
||||
.click();
|
||||
cy.get(".ant-calendar-picker-input").should(
|
||||
"have.value",
|
||||
proposal.milestones[0].date.expect
|
||||
);
|
||||
|
||||
cy.get("button")
|
||||
.contains("Add another milestone")
|
||||
.click({ force: true });
|
||||
|
||||
cy.get('input[name="title"]')
|
||||
.eq(1)
|
||||
.type(proposal.milestones[1].title);
|
||||
cy.get('textarea[name="body"]')
|
||||
.eq(1)
|
||||
.type(proposal.milestones[1].body);
|
||||
cy.get('input[placeholder="Expected completion date"]')
|
||||
.eq(1)
|
||||
.click();
|
||||
cy.get(".ant-calendar-month-panel-next-year-btn").click();
|
||||
cy.get(".ant-calendar-month-panel-month")
|
||||
.contains(proposal.milestones[1].date.m)
|
||||
.click();
|
||||
cy.get(".ant-calendar-picker-input")
|
||||
.eq(1)
|
||||
.should("have.value", proposal.milestones[1].date.expect);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get("@Continue").click();
|
||||
|
||||
// step 5
|
||||
cy.window()
|
||||
.then(w => (w as any).web3.eth.getAccounts())
|
||||
.then(accts => {
|
||||
cy.get('input[name="payOutAddress"]').type(accts[0]);
|
||||
cy.get("button")
|
||||
.contains("Add another trustee")
|
||||
.click({ force: true });
|
||||
cy.get(
|
||||
'input[placeholder="0x8B0B72F8bDE212991135668922fD5acE557DE6aB"]'
|
||||
)
|
||||
.eq(1)
|
||||
.type(accts[1]);
|
||||
cy.get('input[name="deadline"][value="2592000"]').click({
|
||||
force: true
|
||||
});
|
||||
cy.get('input[name="milestoneDeadline"][value="259200"]').click({
|
||||
force: true
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get("@Continue").click();
|
||||
|
||||
// final
|
||||
cy.get("button")
|
||||
.contains("Publish")
|
||||
.click();
|
||||
cy.get(".CreateFinal-loader-text").contains("Deploying contract...");
|
||||
|
||||
cy.get(".CreateFinal-message-text a", { timeout: 20000 })
|
||||
.contains("Click here")
|
||||
.click();
|
||||
|
||||
// done
|
||||
cy.get(".Proposal-top-main-title").contains(proposal.title);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/// <reference types="cypress"/>
|
||||
import {
|
||||
loadWeb3,
|
||||
increaseTime,
|
||||
randomString,
|
||||
syncTimeWithEvm
|
||||
} from "../helpers";
|
||||
|
||||
// describe("sandbox", () => {
|
||||
// it("how to increase time", () => {
|
||||
// cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(0) });
|
||||
// // increase time on browser and ganache
|
||||
// increaseTime(cy, 60000);
|
||||
// });
|
||||
// });
|
|
@ -0,0 +1,107 @@
|
|||
/// <reference types="cypress"/>
|
||||
import { syncTimeWithEvm, loadWeb3, testAccounts } from "./helpers";
|
||||
|
||||
export const authenticateUser = (
|
||||
cy: Cypress.Chainable,
|
||||
accountIndex: number
|
||||
) => {
|
||||
const name = `Qual Itty ${accountIndex}`;
|
||||
const ethAccount = testAccounts[accountIndex][0];
|
||||
const title = `QA Robot ${accountIndex}`;
|
||||
const email = `qa.robot.${accountIndex}@grant.io`;
|
||||
cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(accountIndex) });
|
||||
syncTimeWithEvm(cy);
|
||||
cy.get(".AuthButton").click();
|
||||
cy.request({
|
||||
url: `http://localhost:5000/api/v1/users/${ethAccount}`,
|
||||
method: "GET",
|
||||
failOnStatusCode: false
|
||||
})
|
||||
.its("status")
|
||||
.then(status => {
|
||||
if (status === 200) {
|
||||
cy.contains("button", "Prove identity").click();
|
||||
} else {
|
||||
cy.get("input[name='name']").type(name);
|
||||
cy.get("input[name='title']").type(title);
|
||||
cy.get("input[name='email']").type(email);
|
||||
cy.contains("button", "Claim Identity").click();
|
||||
}
|
||||
cy.contains(".ProfileUser", email);
|
||||
});
|
||||
};
|
||||
|
||||
export const createDemoProposal = (
|
||||
cy: Cypress.Chainable,
|
||||
title: string,
|
||||
amount: string
|
||||
) => {
|
||||
cy.get('[href="/create"]').click();
|
||||
|
||||
// expects to be @ /create
|
||||
cy.url().should("contain", "/create");
|
||||
|
||||
cy.log("CREATE DEMO PROPOSAL", title, amount);
|
||||
|
||||
// demo proposal
|
||||
cy.get("button.CreateFlow-footer-example").click();
|
||||
|
||||
// change name
|
||||
cy.get(".ant-steps > :nth-child(1)").click();
|
||||
cy.get('.CreateFlow input[name="title"]', { timeout: 20000 })
|
||||
.clear()
|
||||
.type(title)
|
||||
.blur();
|
||||
cy.get('.CreateFlow input[name="amountToRaise"]')
|
||||
.clear()
|
||||
.type(amount)
|
||||
.blur();
|
||||
cy.wait(1000);
|
||||
|
||||
// remove extra trustees
|
||||
cy.get(".ant-steps > :nth-child(5)").click();
|
||||
cy.get(
|
||||
":nth-child(11) > .ant-form-item-control-wrapper div > button"
|
||||
).click();
|
||||
cy.get(
|
||||
":nth-child(10) > .ant-form-item-control-wrapper div > button"
|
||||
).click();
|
||||
cy.get(":nth-child(9) > .ant-form-item-control-wrapper div > button").click();
|
||||
cy.get(":nth-child(8) > .ant-form-item-control-wrapper div > button").click();
|
||||
cy.get(":nth-child(7) > .ant-form-item-control-wrapper div > button").click();
|
||||
cy.wait(1000);
|
||||
cy.get(".CreateFlow-footer-button")
|
||||
.contains("Continue")
|
||||
.click();
|
||||
|
||||
// final
|
||||
cy.get("button")
|
||||
.contains("Publish")
|
||||
.click();
|
||||
cy.get(".CreateFinal-loader-text").contains("Deploying contract...");
|
||||
cy.get(".CreateFinal-message-text a", { timeout: 30000 })
|
||||
.contains("Click here")
|
||||
.click();
|
||||
|
||||
// created
|
||||
cy.get(".Proposal-top-main-title").contains(title);
|
||||
};
|
||||
|
||||
export const fundProposal = (
|
||||
cy: Cypress.Chainable,
|
||||
accountIndex: number,
|
||||
amount: number
|
||||
) => {
|
||||
// expects to be @ /proposals/<proposal>
|
||||
cy.url().should("contain", "/proposals/");
|
||||
|
||||
// reload page with accountIndex account
|
||||
syncTimeWithEvm(cy);
|
||||
cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(accountIndex) }));
|
||||
|
||||
// fund proposal
|
||||
cy.get(".ant-input", { timeout: 20000 }).type(amount + "");
|
||||
cy.contains(".ant-form > .ant-btn", "Fund this project", { timeout: 20000 })
|
||||
.click()
|
||||
.should("not.have.attr", "loading");
|
||||
};
|
|
@ -2,3 +2,7 @@ declare module "*.json" {
|
|||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "eth-sig-util" {
|
||||
export function signTypedData(k: any, d: any): string;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@cypress/webpack-preprocessor": "^2.0.1",
|
||||
"@types/web3": "^1.0.3",
|
||||
"cypress": "^3.1.0",
|
||||
"eth-sig-util": "^2.1.0",
|
||||
"ts-loader": "^5.0.0",
|
||||
"typescript": "^3.0.3",
|
||||
"web3": "^1.0.0-beta.36",
|
||||
|
|
115
e2e/yarn.lock
115
e2e/yarn.lock
|
@ -1011,6 +1011,16 @@ binary-extensions@^1.0.0:
|
|||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
|
||||
|
||||
bindings@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
|
||||
|
||||
bip66@^1.1.3:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
bl@^1.0.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
|
||||
|
@ -1040,7 +1050,7 @@ bn.js@4.11.6:
|
|||
version "4.11.6"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.6, bn.js@^4.4.0:
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.6, bn.js@^4.4.0, bn.js@^4.8.0:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
|
||||
|
@ -1100,7 +1110,7 @@ brorand@^1.0.1:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
|
||||
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
|
||||
browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
|
||||
dependencies:
|
||||
|
@ -1213,7 +1223,7 @@ buffer@^4.3.0:
|
|||
ieee754 "^1.1.4"
|
||||
isarray "^1.0.0"
|
||||
|
||||
buffer@^5.0.5:
|
||||
buffer@^5.0.5, buffer@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
|
||||
dependencies:
|
||||
|
@ -1779,6 +1789,14 @@ domain-browser@^1.1.1:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||
|
||||
drbg.js@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b"
|
||||
dependencies:
|
||||
browserify-aes "^1.0.6"
|
||||
create-hash "^1.1.2"
|
||||
create-hmac "^1.1.4"
|
||||
|
||||
duplexer3@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||
|
@ -1820,7 +1838,7 @@ elliptic@6.3.3:
|
|||
hash.js "^1.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
elliptic@^6.0.0, elliptic@^6.4.0:
|
||||
elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a"
|
||||
dependencies:
|
||||
|
@ -1920,6 +1938,46 @@ eth-lib@0.2.7:
|
|||
elliptic "^6.4.0"
|
||||
xhr-request-promise "^0.1.2"
|
||||
|
||||
eth-sig-util@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.1.0.tgz#33e60e5486897a2ddeb4bf5a0993b2c6d5cc9e19"
|
||||
dependencies:
|
||||
buffer "^5.2.1"
|
||||
elliptic "^6.4.0"
|
||||
ethereumjs-abi "0.6.5"
|
||||
ethereumjs-util "^5.1.1"
|
||||
tweetnacl "^1.0.0"
|
||||
tweetnacl-util "^0.15.0"
|
||||
|
||||
ethereumjs-abi@0.6.5:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241"
|
||||
dependencies:
|
||||
bn.js "^4.10.0"
|
||||
ethereumjs-util "^4.3.0"
|
||||
|
||||
ethereumjs-util@^4.3.0:
|
||||
version "4.5.0"
|
||||
resolved "http://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6"
|
||||
dependencies:
|
||||
bn.js "^4.8.0"
|
||||
create-hash "^1.1.2"
|
||||
keccakjs "^0.2.0"
|
||||
rlp "^2.0.0"
|
||||
secp256k1 "^3.0.1"
|
||||
|
||||
ethereumjs-util@^5.1.1:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642"
|
||||
dependencies:
|
||||
bn.js "^4.11.0"
|
||||
create-hash "^1.1.2"
|
||||
ethjs-util "^0.1.3"
|
||||
keccak "^1.0.2"
|
||||
rlp "^2.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
secp256k1 "^3.0.1"
|
||||
|
||||
ethers@4.0.0-beta.1:
|
||||
version "4.0.0-beta.1"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.0-beta.1.tgz#0648268b83e0e91a961b1af971c662cdf8cbab6d"
|
||||
|
@ -1942,6 +2000,13 @@ ethjs-unit@0.1.6:
|
|||
bn.js "4.11.6"
|
||||
number-to-bn "1.7.0"
|
||||
|
||||
ethjs-util@^0.1.3:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536"
|
||||
dependencies:
|
||||
is-hex-prefixed "1.0.0"
|
||||
strip-hex-prefix "1.0.0"
|
||||
|
||||
eventemitter3@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.1.1.tgz#47786bdaa087caf7b1b75e73abc5c7d540158cd0"
|
||||
|
@ -2872,7 +2937,16 @@ jsprim@^1.2.2:
|
|||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
keccakjs@^0.2.1:
|
||||
keccak@^1.0.2:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80"
|
||||
dependencies:
|
||||
bindings "^1.2.1"
|
||||
inherits "^2.0.3"
|
||||
nan "^2.2.1"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
keccakjs@^0.2.0, keccakjs@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/keccakjs/-/keccakjs-0.2.1.tgz#1d633af907ef305bbf9f2fa616d56c44561dfa4d"
|
||||
dependencies:
|
||||
|
@ -3238,6 +3312,10 @@ nan@^2.0.8, nan@^2.3.3, nan@^2.9.2:
|
|||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099"
|
||||
|
||||
nan@^2.2.1:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
||||
|
||||
nano-json-stream-parser@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f"
|
||||
|
@ -3926,6 +4004,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rlp@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.1.0.tgz#e4f9886d5a982174f314543831e36e1a658460f9"
|
||||
dependencies:
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
|
||||
|
@ -3990,6 +4074,19 @@ scryptsy@^1.2.1:
|
|||
dependencies:
|
||||
pbkdf2 "^3.0.3"
|
||||
|
||||
secp256k1@^3.0.1:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.2.tgz#f95f952057310722184fe9c914e6b71281f2f2ae"
|
||||
dependencies:
|
||||
bindings "^1.2.1"
|
||||
bip66 "^1.1.3"
|
||||
bn.js "^4.11.3"
|
||||
create-hash "^1.1.2"
|
||||
drbg.js "^1.0.1"
|
||||
elliptic "^6.2.3"
|
||||
nan "^2.2.1"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
seek-bzip@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"
|
||||
|
@ -4526,10 +4623,18 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tweetnacl-util@^0.15.0:
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz#4576c1cee5e2d63d207fee52f1ba02819480bc75"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
||||
tweetnacl@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.0.tgz#713d8b818da42068740bf68386d0479e66fc8a7b"
|
||||
|
||||
type-is@~1.6.15, type-is@~1.6.16:
|
||||
version "1.6.16"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
|
||||
|
|
|
@ -4,5 +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_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=http://localhost:5000/dev-contracts/CrowdFund.json
|
||||
CROWD_FUND_FACTORY_URL=http://localhost:5000/dev-contracts/CrowdFundFactory.json
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
stories
|
||||
.storybook
|
||||
dist
|
|
@ -1 +1 @@
|
|||
8.11.4
|
||||
8.13.0
|
|
@ -0,0 +1 @@
|
|||
web: yarn start
|
|
@ -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 = () => {
|
||||
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 = () => {
|
||||
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,21 +76,42 @@ 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 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');
|
||||
logMessage(
|
||||
`${name} should have networks[${id}], it has ${actual}`,
|
||||
'error',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true && ok;
|
||||
}, true);
|
||||
res(allHaveId);
|
||||
|
@ -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 {
|
||||
|
|
|
@ -75,7 +75,7 @@ const routeConfigs: RouteConfig[] = [
|
|||
},
|
||||
template: {
|
||||
title: 'Browse proposals',
|
||||
requiresWeb3: true,
|
||||
requiresWeb3: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -86,7 +86,7 @@ const routeConfigs: RouteConfig[] = [
|
|||
},
|
||||
template: {
|
||||
title: 'Proposal',
|
||||
requiresWeb3: true,
|
||||
requiresWeb3: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
import axios from './axios';
|
||||
import { Proposal, TeamMember, Update } from 'types';
|
||||
import { Proposal, TeamMember, Update, Contribution } from 'types';
|
||||
import {
|
||||
formatProposalFromGet,
|
||||
formatTeamMemberForPost,
|
||||
formatTeamMemberFromGet,
|
||||
generateProposalUrl,
|
||||
} from 'utils/api';
|
||||
import { PROPOSAL_CATEGORY } from './constants';
|
||||
|
||||
export function getProposals(): Promise<{ data: Proposal[] }> {
|
||||
return axios.get('/api/v1/proposals/').then(res => {
|
||||
res.data = res.data.map((proposal: any) => {
|
||||
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
||||
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
||||
return proposal;
|
||||
});
|
||||
res.data = res.data.map(formatProposalFromGet);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
export function getProposal(proposalId: number | string): Promise<{ data: Proposal }> {
|
||||
return axios.get(`/api/v1/proposals/${proposalId}`).then(res => {
|
||||
res.data.team = res.data.team.map(formatTeamMemberFromGet);
|
||||
res.data.proposalUrlId = generateProposalUrl(res.data.proposalId, res.data.title);
|
||||
res.data = formatProposalFromGet(res.data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
@ -96,6 +91,16 @@ export function verifyEmail(code: string): Promise<any> {
|
|||
return axios.post(`/api/v1/email/${code}/verify`);
|
||||
}
|
||||
|
||||
export async function fetchCrowdFundFactoryJSON(): Promise<any> {
|
||||
const res = await axios.get(process.env.CROWD_FUND_FACTORY_URL as string);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function fetchCrowdFundJSON(): Promise<any> {
|
||||
const res = await axios.get(process.env.CROWD_FUND_URL as string);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export function postProposalUpdate(
|
||||
proposalId: number,
|
||||
title: string,
|
||||
|
@ -107,6 +112,19 @@ export function postProposalUpdate(
|
|||
});
|
||||
}
|
||||
|
||||
export function postProposalContribution(
|
||||
proposalId: number,
|
||||
txId: string,
|
||||
fromAddress: string,
|
||||
amount: string,
|
||||
): Promise<{ data: Contribution }> {
|
||||
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
||||
txId,
|
||||
fromAddress,
|
||||
amount,
|
||||
});
|
||||
}
|
||||
|
||||
export function postProposalComment(payload: {
|
||||
proposalId: number;
|
||||
parentCommentId?: number;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.MetaMaskRequiredButton {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
background: #f88500;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&-logo {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 1rem 0 0;
|
||||
|
||||
& > img {
|
||||
display: block;
|
||||
height: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { web3Actions } from 'modules/web3';
|
||||
import { Alert } from 'antd';
|
||||
import metaMaskImgSrc from 'static/images/metamask.png';
|
||||
import './index.less';
|
||||
|
||||
interface OwnProps {
|
||||
message: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
isMissingWeb3: boolean;
|
||||
isWeb3Locked: boolean;
|
||||
isWrongNetwork: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
setAccounts: typeof web3Actions['setAccounts'];
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class MetaMaskRequiredButton extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { isMissingWeb3, isWeb3Locked, isWrongNetwork, children, message } = this.props;
|
||||
const displayMessage =
|
||||
((isMissingWeb3 || isWeb3Locked || isWrongNetwork) && message) || null;
|
||||
return (
|
||||
<>
|
||||
{displayMessage}
|
||||
{isMissingWeb3 ? (
|
||||
<a
|
||||
className="MetaMaskRequiredButton"
|
||||
href="https://metamask.io/"
|
||||
target="_blank"
|
||||
rel="noopener nofollow"
|
||||
>
|
||||
<div className="MetaMaskRequiredButton-logo">
|
||||
<img src={metaMaskImgSrc} />
|
||||
</div>
|
||||
MetaMask required
|
||||
</a>
|
||||
) : isWeb3Locked ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={
|
||||
<>
|
||||
It looks like your MetaMask account is locked. Please unlock it and{' '}
|
||||
<a onClick={this.props.setAccounts}>click here to continue</a>.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : isWrongNetwork ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={
|
||||
<>
|
||||
The Grant.io smart contract is currently only supported on the{' '}
|
||||
<strong>Ropsten</strong> network. Please change your network to continue.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||
state => ({
|
||||
isMissingWeb3: state.web3.isMissingWeb3,
|
||||
isWeb3Locked: state.web3.isWeb3Locked,
|
||||
isWrongNetwork: state.web3.isWrongNetwork,
|
||||
}),
|
||||
{
|
||||
setAccounts: web3Actions.setAccounts,
|
||||
},
|
||||
)(MetaMaskRequiredButton);
|
|
@ -0,0 +1,74 @@
|
|||
@small-query: ~'(max-width: 500px)';
|
||||
|
||||
.AvatarEdit {
|
||||
&-avatar {
|
||||
position: relative;
|
||||
height: 10.5rem;
|
||||
width: 10.5rem;
|
||||
margin-right: 1.25rem;
|
||||
align-self: start;
|
||||
|
||||
@media @small-query {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
&-change {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-delete {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 0.2rem;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
import React from 'react';
|
||||
import axios from 'api/axios';
|
||||
import { Upload, Icon, Modal, Button, Alert } from 'antd';
|
||||
import Cropper from 'react-cropper';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
import { UploadFile } from 'antd/lib/upload/interface';
|
||||
import { TeamMember } from 'types';
|
||||
import { getBase64 } from 'utils/blob';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import './AvatarEdit.less';
|
||||
|
||||
const FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
const FILE_MAX_LOAD_MB = 10;
|
||||
|
||||
interface OwnProps {
|
||||
user: TeamMember;
|
||||
onDelete(): void;
|
||||
onDone(url: string): void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
isUploading: false,
|
||||
showModal: false,
|
||||
newAvatarUrl: '',
|
||||
loadError: '',
|
||||
uploadError: '',
|
||||
};
|
||||
type State = typeof initialState;
|
||||
|
||||
type Props = OwnProps;
|
||||
|
||||
export default class AvatarEdit extends React.PureComponent<Props, State> {
|
||||
state = initialState;
|
||||
cropperRef: React.RefObject<any>;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.cropperRef = React.createRef();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { newAvatarUrl, showModal, loadError, uploadError, isUploading } = this.state;
|
||||
const {
|
||||
user,
|
||||
user: { avatarUrl },
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
{' '}
|
||||
<div className="AvatarEdit-avatar">
|
||||
<UserAvatar className="AvatarEdit-avatar-img" user={user} />
|
||||
<Upload
|
||||
name="avatar"
|
||||
showUploadList={false}
|
||||
action={this.handleLoad}
|
||||
beforeUpload={this.beforeLoad}
|
||||
onChange={this.handleLoadChange}
|
||||
>
|
||||
<Button className="AvatarEdit-avatar-change">
|
||||
<Icon
|
||||
className="AvatarEdit-avatar-change-icon"
|
||||
type={avatarUrl ? 'picture' : 'plus-circle'}
|
||||
/>
|
||||
<div>{avatarUrl ? 'Change photo' : 'Add photo'}</div>
|
||||
</Button>
|
||||
</Upload>
|
||||
{avatarUrl && (
|
||||
<Button
|
||||
className="AvatarEdit-avatar-delete"
|
||||
icon="delete"
|
||||
shape="circle"
|
||||
onClick={this.props.onDelete}
|
||||
/>
|
||||
)}
|
||||
{loadError && (
|
||||
<Alert message={loadError} type="error" style={{ margin: '0.5rem 0 0 0' }} />
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
title="Prepare your avatar"
|
||||
visible={showModal}
|
||||
footer={[
|
||||
<Button key="back" onClick={this.handleClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isUploading}
|
||||
onClick={this.handleUpload}
|
||||
>
|
||||
Upload
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Cropper
|
||||
ref={this.cropperRef}
|
||||
src={newAvatarUrl}
|
||||
style={{ height: 300 }}
|
||||
aspectRatio={1}
|
||||
guides={false}
|
||||
viewMode={1}
|
||||
/>
|
||||
{uploadError && (
|
||||
<Alert
|
||||
message={uploadError}
|
||||
type="error"
|
||||
style={{ margin: '0.5rem 0 0 0' }}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private handleClose = () => {
|
||||
this.setState({
|
||||
isUploading: false,
|
||||
showModal: false,
|
||||
newAvatarUrl: '',
|
||||
uploadError: '',
|
||||
});
|
||||
};
|
||||
|
||||
private handleLoadChange = (info: any) => {
|
||||
if (info.file.status === 'done') {
|
||||
getBase64(info.file.originFileObj, newAvatarUrl =>
|
||||
this.setState({
|
||||
newAvatarUrl,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private beforeLoad = (file: UploadFile) => {
|
||||
this.setState({ loadError: '' });
|
||||
const isTypeOk = !!FILE_TYPES.find(t => t === file.type);
|
||||
if (!isTypeOk) {
|
||||
this.setState({ loadError: 'File must be a jpg, png or gif' });
|
||||
}
|
||||
const isSizeOk = file.size / 1024 / 1024 < FILE_MAX_LOAD_MB;
|
||||
if (!isSizeOk) {
|
||||
this.setState({
|
||||
loadError: `File size must be less than ${FILE_MAX_LOAD_MB}MB`,
|
||||
});
|
||||
}
|
||||
return isTypeOk && isSizeOk;
|
||||
};
|
||||
|
||||
private handleLoad = () => {
|
||||
this.setState({ showModal: true });
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
private handleUpload = () => {
|
||||
this.cropperRef.current
|
||||
.getCroppedCanvas({ width: 400, height: 400 })
|
||||
.toBlob((blob: Blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob);
|
||||
this.setState({ isUploading: true });
|
||||
axios
|
||||
.post('/api/v1/users/avatar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then(res => {
|
||||
this.props.onDone(res.data.url);
|
||||
this.handleClose();
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ isUploading: false, uploadError: err.message });
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -7,12 +7,12 @@
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.ProfileEdit {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
z-index: 901;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
|
@ -28,77 +28,6 @@
|
|||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
position: relative;
|
||||
height: 10.5rem;
|
||||
width: 10.5rem;
|
||||
margin-right: 1.25rem;
|
||||
align-self: start;
|
||||
|
||||
@media @small-query {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
&-change {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-delete {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 0.2rem;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
flex: 1;
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import lodash from 'lodash';
|
||||
import { Input, Form, Col, Row, Button, Icon, Alert } from 'antd';
|
||||
import axios from 'api/axios';
|
||||
import { Input, Form, Col, Row, Button, Alert } from 'antd';
|
||||
import { SOCIAL_INFO } from 'utils/social';
|
||||
import { SOCIAL_TYPE, TeamMember } from 'types';
|
||||
import { UserState } from 'modules/users/reducers';
|
||||
import { getCreateTeamMemberError } from 'modules/create/utils';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import AvatarEdit from './AvatarEdit';
|
||||
import './ProfileEdit.less';
|
||||
|
||||
interface Props {
|
||||
|
@ -54,27 +55,12 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<>
|
||||
<div className="ProfileEdit">
|
||||
<div className="ProfileEdit-avatar">
|
||||
<UserAvatar className="ProfileEdit-avatar-img" user={fields} />
|
||||
<Button
|
||||
className="ProfileEdit-avatar-change"
|
||||
onClick={this.handleChangePhoto}
|
||||
>
|
||||
<Icon
|
||||
className="ProfileEdit-avatar-change-icon"
|
||||
type={fields.avatarUrl ? 'picture' : 'plus-circle'}
|
||||
<AvatarEdit
|
||||
user={fields}
|
||||
onDone={this.handleChangePhoto}
|
||||
onDelete={this.handleDeletePhoto}
|
||||
/>
|
||||
<div>{fields.avatarUrl ? 'Change photo' : 'Add photo'}</div>
|
||||
</Button>
|
||||
{fields.avatarUrl && (
|
||||
<Button
|
||||
className="ProfileEdit-avatar-delete"
|
||||
icon="delete"
|
||||
shape="circle"
|
||||
onClick={this.handleDeletePhoto}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ProfileEdit-info">
|
||||
<Form
|
||||
className="ProfileEdit-info-form"
|
||||
|
@ -187,6 +173,13 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
private handleCancel = () => {
|
||||
const { avatarUrl } = this.state.fields;
|
||||
// cleanup uploaded file if we cancel
|
||||
if (this.props.user.avatarUrl !== avatarUrl && avatarUrl) {
|
||||
axios.delete('/api/v1/users/avatar', {
|
||||
params: { url: avatarUrl },
|
||||
});
|
||||
}
|
||||
this.props.onDone();
|
||||
};
|
||||
|
||||
|
@ -226,13 +219,10 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private handleChangePhoto = () => {
|
||||
// TODO: Actual file uploading
|
||||
const gender = ['men', 'women'][Math.floor(Math.random() * 2)];
|
||||
const num = Math.floor(Math.random() * 80);
|
||||
private handleChangePhoto = (url: string) => {
|
||||
const fields = {
|
||||
...this.state.fields,
|
||||
avatarUrl: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
|
||||
avatarUrl: url,
|
||||
};
|
||||
const isChanged = this.isChangedCheck(fields);
|
||||
this.setState({
|
||||
|
|
|
@ -4,17 +4,17 @@ import { Spin, Form, Input, Button, Icon } from 'antd';
|
|||
import { ProposalWithCrowdFund } from 'types';
|
||||
import './style.less';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { fromWei } from 'utils/units';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'recompose';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { web3Actions } from 'modules/web3';
|
||||
import { withRouter } from 'react-router';
|
||||
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
||||
import ShortAddress from 'components/ShortAddress';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import { getAmountError } from 'utils/validators';
|
||||
import { CATEGORY_UI } from 'api/constants';
|
||||
import MetaMaskRequiredButton from 'components/MetaMaskRequiredButton';
|
||||
|
||||
interface OwnProps {
|
||||
proposal: ProposalWithCrowdFund;
|
||||
|
@ -29,11 +29,7 @@ interface ActionProps {
|
|||
fundCrowdFund: typeof web3Actions['fundCrowdFund'];
|
||||
}
|
||||
|
||||
interface Web3Props {
|
||||
web3: Web3RenderProps['web3'];
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & ActionProps & Web3Props;
|
||||
type Props = OwnProps & StateProps & ActionProps;
|
||||
|
||||
interface State {
|
||||
amountToRaise: string;
|
||||
|
@ -58,7 +54,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { proposal, web3 } = this.props;
|
||||
const { proposal } = this.props;
|
||||
const { crowdFund } = proposal;
|
||||
const remainingTarget = crowdFund.target.sub(crowdFund.funded);
|
||||
const amount = parseFloat(value);
|
||||
|
@ -67,7 +63,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
if (Number.isNaN(amount)) {
|
||||
// They're entering some garbage, they’ll work it out
|
||||
} else {
|
||||
const remainingEthNum = parseFloat(web3.utils.fromWei(remainingTarget, 'ether'));
|
||||
const remainingEthNum = parseFloat(fromWei(remainingTarget, 'ether'));
|
||||
amountError = getAmountError(amount, remainingEthNum);
|
||||
}
|
||||
|
||||
|
@ -82,7 +78,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { proposal, sendLoading, web3, isPreview } = this.props;
|
||||
const { proposal, sendLoading, isPreview } = this.props;
|
||||
const { amountToRaise, amountError } = this.state;
|
||||
const amountFloat = parseFloat(amountToRaise) || 0;
|
||||
let content;
|
||||
|
@ -94,7 +90,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
crowdFund.isFrozen;
|
||||
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
|
||||
const remainingEthNum = parseFloat(
|
||||
web3.utils.fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
||||
fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
||||
);
|
||||
|
||||
content = (
|
||||
|
@ -166,7 +162,21 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form layout="vertical">
|
||||
<MetaMaskRequiredButton
|
||||
message={
|
||||
<Form.Item style={{ marginBottom: '0.5rem', paddingBottom: 0 }}>
|
||||
<Input
|
||||
size="large"
|
||||
type="number"
|
||||
placeholder="0.5"
|
||||
addonAfter="ETH"
|
||||
disabled={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
validateStatus={amountError ? 'error' : undefined}
|
||||
help={amountError}
|
||||
|
@ -186,7 +196,6 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
disabled={isPreview}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
onClick={this.sendTransaction}
|
||||
size="large"
|
||||
|
@ -197,6 +206,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
>
|
||||
Fund this project
|
||||
</Button>
|
||||
</MetaMaskRequiredButton>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
@ -226,21 +236,9 @@ const withConnect = connect(
|
|||
{ fundCrowdFund: web3Actions.fundCrowdFund },
|
||||
);
|
||||
|
||||
const ConnectedProposalCampaignBlock = compose<Props, OwnProps & Web3Props>(
|
||||
const ConnectedProposalCampaignBlock = compose<Props, OwnProps>(
|
||||
withRouter,
|
||||
withConnect,
|
||||
)(ProposalCampaignBlock);
|
||||
|
||||
export default (props: OwnProps) => (
|
||||
<Web3Container
|
||||
renderLoading={() => (
|
||||
<div className="ProposalCampaignBlock Proposal-top-side-block">
|
||||
<h1 className="Proposal-top-main-block-title">Campaign</h1>
|
||||
<div className="Proposal-top-main-block">
|
||||
<Spin />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
render={({ web3 }) => <ConnectedProposalCampaignBlock {...props} web3={web3} />}
|
||||
/>
|
||||
);
|
||||
export default ConnectedProposalCampaignBlock;
|
||||
|
|
|
@ -18,12 +18,11 @@ import ContributorsTab from './Contributors';
|
|||
// import CommunityTab from './Community';
|
||||
import UpdateModal from './UpdateModal';
|
||||
import CancelModal from './CancelModal';
|
||||
import './style.less';
|
||||
import classnames from 'classnames';
|
||||
import { withRouter } from 'react-router';
|
||||
import Web3Container from 'lib/Web3Container';
|
||||
import { web3Actions } from 'modules/web3';
|
||||
import SocialShare from 'components/SocialShare';
|
||||
import './style.less';
|
||||
|
||||
interface OwnProps {
|
||||
proposalId: number;
|
||||
|
@ -32,17 +31,14 @@ interface OwnProps {
|
|||
|
||||
interface StateProps {
|
||||
proposal: ProposalWithCrowdFund | null;
|
||||
account: string | null;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
fetchProposal: proposalActions.TFetchProposal;
|
||||
}
|
||||
|
||||
interface Web3Props {
|
||||
account: string;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & Web3Props & OwnProps;
|
||||
type Props = StateProps & DispatchProps & OwnProps;
|
||||
|
||||
interface State {
|
||||
isBodyExpanded: boolean;
|
||||
|
@ -67,12 +63,16 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
} else {
|
||||
this.checkBodyOverflow();
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', this.checkBodyOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', this.checkBodyOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.proposal) {
|
||||
|
@ -95,7 +95,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
return <Spin />;
|
||||
} else {
|
||||
const { crowdFund } = proposal;
|
||||
const isTrustee = crowdFund.trustees.includes(account);
|
||||
const isTrustee = !!account && crowdFund.trustees.includes(account);
|
||||
const isContributor = !!crowdFund.contributors.find(c => c.address === account);
|
||||
const hasBeenFunded = crowdFund.isRaiseGoalReached;
|
||||
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
|
||||
|
@ -125,7 +125,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
<div className="Proposal-top">
|
||||
<div className="Proposal-top-social">
|
||||
<SocialShare
|
||||
url={window.location.href}
|
||||
url={(typeof window !== 'undefined' && window.location.href) || ''}
|
||||
title={`${proposal.title} needs funding on Grant-io!`}
|
||||
text={`${
|
||||
proposal.title
|
||||
|
@ -251,6 +251,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
function mapStateToProps(state: AppState, ownProps: OwnProps) {
|
||||
return {
|
||||
proposal: getProposal(state, ownProps.proposalId),
|
||||
account: (state.web3.accounts.length && state.web3.accounts[0]) || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -263,22 +264,9 @@ const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
|
|||
mapDispatchToProps,
|
||||
);
|
||||
|
||||
const ConnectedProposal = compose<Props, OwnProps & Web3Props>(
|
||||
const ConnectedProposal = compose<Props, OwnProps>(
|
||||
withRouter,
|
||||
withConnect,
|
||||
)(ProposalDetail);
|
||||
|
||||
export default (props: OwnProps) => (
|
||||
<Web3Container
|
||||
renderLoading={() => (
|
||||
<div className="Proposal">
|
||||
<div className="Proposal-top">
|
||||
<div className="Proposal-top-main">
|
||||
<Spin />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
render={({ accounts }) => <ConnectedProposal account={accounts[0]} {...props} />}
|
||||
/>
|
||||
);
|
||||
export default ConnectedProposal;
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Progress, Icon, Spin } from 'antd';
|
||||
import { Progress, Icon } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { CATEGORY_UI } from 'api/constants';
|
||||
import { ProposalWithCrowdFund } from 'types';
|
||||
import './style.less';
|
||||
import { Dispatch, bindActionCreators } from 'redux';
|
||||
import * as web3Actions from 'modules/web3/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { connect } from 'react-redux';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import './style.less';
|
||||
|
||||
interface Props extends ProposalWithCrowdFund {
|
||||
web3: AppState['web3']['web3'];
|
||||
}
|
||||
|
||||
export class ProposalCard extends React.Component<Props> {
|
||||
export class ProposalCard extends React.Component<ProposalWithCrowdFund> {
|
||||
state = { redirect: '' };
|
||||
render() {
|
||||
if (this.state.redirect) {
|
||||
|
@ -29,14 +21,10 @@ export class ProposalCard extends React.Component<Props> {
|
|||
proposalUrlId,
|
||||
category,
|
||||
dateCreated,
|
||||
web3,
|
||||
crowdFund,
|
||||
team,
|
||||
} = this.props;
|
||||
|
||||
if (!web3) {
|
||||
return <Spin />;
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="ProposalCard"
|
||||
|
@ -45,8 +33,8 @@ export class ProposalCard extends React.Component<Props> {
|
|||
<h3 className="ProposalCard-title">{title}</h3>
|
||||
<div className="ProposalCard-funding">
|
||||
<div className="ProposalCard-funding-raised">
|
||||
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small>{' '}
|
||||
of <UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
|
||||
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small> of{' '}
|
||||
<UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
|
||||
</div>
|
||||
<div
|
||||
className={classnames({
|
||||
|
@ -94,19 +82,5 @@ export class ProposalCard extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return bindActionCreators(web3Actions, dispatch);
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
web3: state.web3.web3,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ProposalCard);
|
||||
export default ProposalCard;
|
||||
|
|
|
@ -5,11 +5,10 @@ import { getProposals } from 'modules/proposals/selectors';
|
|||
import { ProposalWithCrowdFund } from 'types';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Input, Divider, Spin, Drawer, Icon, Button } from 'antd';
|
||||
import { Input, Divider, Drawer, Icon, Button } from 'antd';
|
||||
import ProposalResults from './Results';
|
||||
import ProposalFilters, { Filters } from './Filters';
|
||||
import { PROPOSAL_SORT } from 'api/constants';
|
||||
import Web3Container from 'lib/Web3Container';
|
||||
import './style.less';
|
||||
|
||||
type ProposalSortFn = (p1: ProposalWithCrowdFund, p2: ProposalWithCrowdFund) => number;
|
||||
|
@ -246,6 +245,4 @@ const ConnectedProposals = connect(
|
|||
mapDispatchToProps,
|
||||
)(Proposals);
|
||||
|
||||
export default () => (
|
||||
<Web3Container renderLoading={() => <Spin />} render={() => <ConnectedProposals />} />
|
||||
);
|
||||
export default ConnectedProposals;
|
||||
|
|
|
@ -6,12 +6,20 @@ import { loadComponents } from 'loadable-components';
|
|||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { configureStore } from 'store/configure';
|
||||
import { massageSerializedState } from 'utils/api';
|
||||
import Routes from './Routes';
|
||||
import i18n from './i18n';
|
||||
|
||||
const initialState = window && (window as any).__PRELOADED_STATE__;
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
release: process.env.SENTRY_RELEASE,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
const initialState =
|
||||
window && massageSerializedState((window as any).__PRELOADED_STATE__);
|
||||
const { store, persistor } = configureStore(initialState);
|
||||
const i18nLanguage = window && (window as any).__PRELOADED_I18N__;
|
||||
i18n.changeLanguage(i18nLanguage.locale);
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import Web3 from 'web3';
|
||||
import getContractInstance from './getContract';
|
||||
import { fetchCrowdFundJSON } from 'api/api';
|
||||
|
||||
const contractCache = {} as { [key: string]: any };
|
||||
|
||||
export async function getCrowdFundContract(web3: Web3 | null, deployedAddress: string) {
|
||||
if (!web3) {
|
||||
throw new Error('getCrowdFundAddress: web3 was null but is required!');
|
||||
}
|
||||
if (!contractCache[deployedAddress]) {
|
||||
let CrowdFund;
|
||||
CrowdFund = await fetchCrowdFundJSON();
|
||||
try {
|
||||
contractCache[deployedAddress] = await getContractInstance(
|
||||
web3,
|
||||
CrowdFund,
|
||||
deployedAddress,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`Could not lookup crowdFund contract @ ${deployedAddress}: `, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return contractCache[deployedAddress];
|
||||
}
|
|
@ -10,13 +10,23 @@ 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;
|
||||
|
||||
// create the instance
|
||||
return new web3.eth.Contract(contractDefinition.abi, deployedAddress);
|
||||
const contract = new web3.eth.Contract(contractDefinition.abi, deployedAddress);
|
||||
|
||||
// use gas from e2e injected window.web3.provider
|
||||
if ((web3.currentProvider as any)._e2eContractGas) {
|
||||
contract.options.gas = (web3.currentProvider as any)._e2eContractGas;
|
||||
}
|
||||
return contract;
|
||||
};
|
||||
|
||||
export default getContractInstance;
|
||||
|
|
|
@ -10,7 +10,6 @@ const resolveWeb3 = (resolve: (web3: Web3) => void, reject: (err: Error) => void
|
|||
}
|
||||
|
||||
let { web3 } = window as Web3Window;
|
||||
const localProvider = `http://localhost:8545`;
|
||||
|
||||
// To test what it's like to not have web3, uncomment the reject. Otherwise
|
||||
// localProvider will always kick in.
|
||||
|
@ -19,10 +18,6 @@ const resolveWeb3 = (resolve: (web3: Web3) => void, reject: (err: Error) => void
|
|||
if (typeof web3 !== 'undefined') {
|
||||
console.info(`Injected web3 detected.`);
|
||||
web3 = new Web3(web3.currentProvider);
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
console.info(`No web3 instance injected, using Local web3.`);
|
||||
const provider = new Web3.providers.HttpProvider(localProvider);
|
||||
web3 = new Web3(provider);
|
||||
} else {
|
||||
return reject(new Error('No web3 instance available'));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import types from './types';
|
||||
import { Dispatch } from 'redux';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { sleep } from 'utils/helpers';
|
||||
import { generateAuthSignatureData } from 'utils/auth';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
@ -37,7 +38,13 @@ export function authUser(address: string, authSignature?: Falsy | AuthSignatureD
|
|||
signedMessage: authSignature.signedMessage,
|
||||
rawTypedData: JSON.stringify(authSignature.rawTypedData),
|
||||
});
|
||||
|
||||
// sentry user scope
|
||||
Sentry.configureScope(scope => {
|
||||
scope.setUser({
|
||||
email: res.data.emailAddress,
|
||||
accountAddress: res.data.ethAddress,
|
||||
});
|
||||
});
|
||||
dispatch({
|
||||
type: types.AUTH_USER_FULFILLED,
|
||||
payload: {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import types from './types';
|
||||
import usersTypes from 'modules/users/types';
|
||||
// TODO: Use a common User type instead of this
|
||||
import { TeamMember, AuthSignatureData } from 'types';
|
||||
|
||||
|
@ -56,6 +57,14 @@ export default function createReducer(
|
|||
authSignatureAddress: action.payload.user.ethAddress,
|
||||
isAuthingUser: false,
|
||||
};
|
||||
case usersTypes.UPDATE_USER_FULFILLED:
|
||||
return {
|
||||
...state,
|
||||
user:
|
||||
state.user && state.user.ethAddress === action.payload.user.ethAddress
|
||||
? action.payload.user
|
||||
: state.user,
|
||||
};
|
||||
case types.AUTH_USER_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -254,6 +254,5 @@ export function makeProposalPreviewFromForm(
|
|||
isFrozen: false,
|
||||
isRaiseGoalReached: false,
|
||||
},
|
||||
crowdFundContract: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,67 +4,20 @@ import {
|
|||
getProposal,
|
||||
getProposalComments,
|
||||
getProposalUpdates,
|
||||
postProposalContribution as apiPostProposalContribution,
|
||||
postProposalComment as apiPostProposalComment,
|
||||
} from 'api/api';
|
||||
import { Dispatch } from 'redux';
|
||||
import Web3 from 'web3';
|
||||
import { ProposalWithCrowdFund, Proposal, Comment, AuthSignatureData } from 'types';
|
||||
import { ProposalWithCrowdFund, Comment, AuthSignatureData } from 'types';
|
||||
import { signData } from 'modules/web3/actions';
|
||||
import getContract from 'lib/getContract';
|
||||
import CrowdFund from 'lib/contracts/CrowdFund.json';
|
||||
import { getCrowdFundState } from 'web3interact/crowdFund';
|
||||
|
||||
async function getMergedCrowdFundProposal(
|
||||
proposal: Proposal,
|
||||
web3: Web3,
|
||||
account: string,
|
||||
) {
|
||||
const crowdFundContract = await getContract(web3, CrowdFund, proposal.proposalAddress);
|
||||
const crowdFundData = {
|
||||
crowdFundContract,
|
||||
crowdFund: await getCrowdFundState(crowdFundContract, account, web3),
|
||||
};
|
||||
|
||||
for (let i = 0; i < crowdFundData.crowdFund.milestones.length; i++) {
|
||||
proposal.milestones[i] = {
|
||||
...proposal.milestones[i],
|
||||
...crowdFundData.crowdFund.milestones[i],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...crowdFundData,
|
||||
...proposal,
|
||||
};
|
||||
}
|
||||
|
||||
// valid as defined by crowdFund contract existing on current network
|
||||
export async function getValidProposals(
|
||||
proposals: { data: Proposal[] },
|
||||
web3: Web3,
|
||||
account: string,
|
||||
) {
|
||||
return (await Promise.all(
|
||||
proposals.data.map(async (proposal: Proposal) => {
|
||||
try {
|
||||
return await getMergedCrowdFundProposal(proposal, web3, account);
|
||||
} catch (e) {
|
||||
console.error('Could not lookup crowdFund contract', e);
|
||||
}
|
||||
}),
|
||||
// remove proposals that except since they cannot be retrieved via getContract
|
||||
)).filter(Boolean);
|
||||
}
|
||||
|
||||
export type TFetchProposals = typeof fetchProposals;
|
||||
export function fetchProposals() {
|
||||
return (dispatch: Dispatch<any>, getState: any) => {
|
||||
const state = getState();
|
||||
return (dispatch: Dispatch<any>) => {
|
||||
return dispatch({
|
||||
type: types.PROPOSALS_DATA,
|
||||
payload: async () => {
|
||||
const proposals = await getProposals();
|
||||
return getValidProposals(proposals, state.web3.web3, state.web3.accounts[0]);
|
||||
return (await getProposals()).data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -72,17 +25,11 @@ export function fetchProposals() {
|
|||
|
||||
export type TFetchProposal = typeof fetchProposal;
|
||||
export function fetchProposal(proposalId: ProposalWithCrowdFund['proposalId']) {
|
||||
return (dispatch: Dispatch<any>, getState: any) => {
|
||||
const state = getState();
|
||||
dispatch({
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
return dispatch({
|
||||
type: types.PROPOSAL_DATA,
|
||||
payload: async () => {
|
||||
const proposal = await getProposal(proposalId);
|
||||
return await getMergedCrowdFundProposal(
|
||||
proposal.data,
|
||||
state.web3.web3,
|
||||
state.web3.accounts[0],
|
||||
);
|
||||
return (await getProposal(proposalId)).data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -158,3 +105,17 @@ export function postProposalComment(
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function postProposalContribution(
|
||||
proposalId: number,
|
||||
txId: string,
|
||||
account: string,
|
||||
amount: string,
|
||||
) {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
await dispatch({
|
||||
type: types.POST_PROPOSAL_CONTRIBUTION,
|
||||
payload: apiPostProposalContribution(proposalId, txId, account, amount),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
enum clockTypes {
|
||||
enum proposalTypes {
|
||||
PROPOSALS_DATA = 'PROPOSALS_DATA',
|
||||
PROPOSALS_DATA_FULFILLED = 'PROPOSALS_DATA_FULFILLED',
|
||||
PROPOSALS_DATA_REJECTED = 'PROPOSALS_DATA_REJECTED',
|
||||
|
@ -23,6 +23,8 @@ enum clockTypes {
|
|||
POST_PROPOSAL_COMMENT_FULFILLED = 'POST_PROPOSAL_COMMENT_FULFILLED',
|
||||
POST_PROPOSAL_COMMENT_REJECTED = 'POST_PROPOSAL_COMMENT_REJECTED',
|
||||
POST_PROPOSAL_COMMENT_PENDING = 'POST_PROPOSAL_COMMENT_PENDING',
|
||||
|
||||
POST_PROPOSAL_CONTRIBUTION = 'POST_PROPOSAL_CONTRIBUTION',
|
||||
}
|
||||
|
||||
export default clockTypes;
|
||||
export default proposalTypes;
|
||||
|
|
|
@ -5,10 +5,15 @@ import { postProposal } from 'api/api';
|
|||
import getContract, { WrongNetworkError } from 'lib/getContract';
|
||||
import { sleep } from 'utils/helpers';
|
||||
import { web3ErrorToString } from 'utils/web3';
|
||||
import { fetchProposal, fetchProposals } from 'modules/proposals/actions';
|
||||
import {
|
||||
fetchProposal,
|
||||
fetchProposals,
|
||||
postProposalContribution,
|
||||
} from 'modules/proposals/actions';
|
||||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Wei } from 'utils/units';
|
||||
import { getCrowdFundContract } from 'lib/crowdFundContracts';
|
||||
import { TeamMember, AuthSignatureData, ProposalWithCrowdFund } from 'types';
|
||||
|
||||
type GetState = () => AppState;
|
||||
|
@ -201,7 +206,12 @@ export function requestMilestonePayout(proposal: ProposalWithCrowdFund, index: n
|
|||
});
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await crowdFundContract.methods
|
||||
.requestMilestonePayout(index)
|
||||
|
@ -231,7 +241,11 @@ export function payMilestonePayout(proposal: ProposalWithCrowdFund, index: numbe
|
|||
});
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await crowdFundContract.methods
|
||||
|
@ -265,7 +279,16 @@ export function fundCrowdFund(proposal: ProposalWithCrowdFund, value: number | s
|
|||
const state = getState();
|
||||
const web3 = state.web3.web3;
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(web3, proposalAddress);
|
||||
|
||||
const handleErr = (err: Error) => {
|
||||
dispatch({
|
||||
type: types.SEND_REJECTED,
|
||||
payload: err.message || err.toString(),
|
||||
error: true,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (!web3) {
|
||||
|
@ -274,20 +297,27 @@ export function fundCrowdFund(proposal: ProposalWithCrowdFund, value: number | s
|
|||
await crowdFundContract.methods
|
||||
.contribute()
|
||||
.send({ from: account, value: web3.utils.toWei(String(value), 'ether') })
|
||||
.once('confirmation', async () => {
|
||||
.once('confirmation', async (_: number, receipt: any) => {
|
||||
try {
|
||||
await sleep(5000);
|
||||
await dispatch(
|
||||
postProposalContribution(
|
||||
proposalId,
|
||||
receipt.transactionHash,
|
||||
account,
|
||||
String(value),
|
||||
),
|
||||
);
|
||||
await dispatch(fetchProposal(proposalId));
|
||||
dispatch({
|
||||
type: types.SEND_FULFILLED,
|
||||
});
|
||||
} catch (err) {
|
||||
handleErr(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
dispatch({
|
||||
type: types.SEND_REJECTED,
|
||||
payload: err.message || err.toString(),
|
||||
error: true,
|
||||
});
|
||||
handleErr(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -301,7 +331,11 @@ export function voteMilestonePayout(
|
|||
dispatch({ type: types.VOTE_AGAINST_MILESTONE_PAYOUT_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await crowdFundContract.methods
|
||||
|
@ -328,7 +362,11 @@ export function voteRefund(proposal: ProposalWithCrowdFund, vote: boolean) {
|
|||
dispatch({ type: types.VOTE_REFUND_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await crowdFundContract.methods
|
||||
|
@ -377,7 +415,11 @@ export function triggerRefund(proposal: ProposalWithCrowdFund) {
|
|||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await freezeContract(crowdFundContract, account);
|
||||
|
@ -398,7 +440,11 @@ export function withdrawRefund(proposal: ProposalWithCrowdFund, address: string)
|
|||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
const { crowdFundContract, proposalId } = proposal;
|
||||
const { proposalAddress, proposalId } = proposal;
|
||||
const crowdFundContract = await getCrowdFundContract(
|
||||
state.web3.web3,
|
||||
proposalAddress,
|
||||
);
|
||||
|
||||
try {
|
||||
await freezeContract(crowdFundContract, account);
|
||||
|
@ -462,7 +508,7 @@ export function signData(data: object, dataTypes: object, primaryType: string) {
|
|||
primaryType,
|
||||
};
|
||||
|
||||
(web3.currentProvider as any).sendAsync(
|
||||
(web3.currentProvider as any).send(
|
||||
{
|
||||
method: 'eth_signTypedData_v3',
|
||||
params: [accounts[0], JSON.stringify(rawTypedData)],
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import { SagaIterator } from 'redux-saga';
|
||||
import { put, all, fork, take, takeLatest, select, call } from 'redux-saga/effects';
|
||||
import { setWeb3, setAccounts, setContract } from './actions';
|
||||
import { all, call, fork, put, select, take, takeLatest } from 'redux-saga/effects';
|
||||
import { setAccounts, setContract, setWeb3 } from './actions';
|
||||
import { selectWeb3 } from './selectors';
|
||||
import { safeEnable } from 'utils/web3';
|
||||
import types from './types';
|
||||
|
||||
/* tslint:disable no-var-requires --- TODO: find a better way to import contract */
|
||||
const CrowdFundFactory = require('lib/contracts/CrowdFundFactory.json');
|
||||
import { fetchCrowdFundFactoryJSON } from 'api/api';
|
||||
|
||||
export function* bootstrapWeb3(): SagaIterator {
|
||||
// Don't attempt to bootstrap web3 on SSR
|
||||
if (process.env.SERVER_SIDE_RENDER) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CrowdFundFactory = yield call(fetchCrowdFundFactoryJSON);
|
||||
yield put<any>(setWeb3());
|
||||
yield take(types.WEB3_FULFILLED);
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ const sagaMiddleware = createSagaMiddleware();
|
|||
type MiddleWare = ThunkMiddleware | SagaMiddleware<any> | any;
|
||||
|
||||
const bindMiddleware = (middleware: MiddleWare[]) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
|
||||
const { createLogger } = require('redux-logger');
|
||||
const logger = createLogger({
|
||||
collapsed: true,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { TeamMember } from 'types';
|
||||
import BN from 'bn.js';
|
||||
import { TeamMember, CrowdFund, ProposalWithCrowdFund, UserProposal } from 'types';
|
||||
import { socialAccountsToUrls, socialUrlsToAccounts } from 'utils/social';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
export function formatTeamMemberForPost(user: TeamMember) {
|
||||
return {
|
||||
|
@ -27,6 +29,38 @@ export function formatTeamMemberFromGet(user: any): TeamMember {
|
|||
};
|
||||
}
|
||||
|
||||
export function formatCrowdFundFromGet(crowdFund: CrowdFund, base = 10): CrowdFund {
|
||||
const bnKeys = ['amountVotingForRefund', 'balance', 'funded', 'target'] as Array<
|
||||
keyof CrowdFund
|
||||
>;
|
||||
bnKeys.forEach(k => {
|
||||
crowdFund[k] = new BN(crowdFund[k] as string, base);
|
||||
});
|
||||
crowdFund.milestones = crowdFund.milestones.map(ms => {
|
||||
ms.amount = new BN(ms.amount, base);
|
||||
ms.amountAgainstPayout = new BN(ms.amountAgainstPayout, base);
|
||||
return ms;
|
||||
});
|
||||
crowdFund.contributors = crowdFund.contributors.map(c => {
|
||||
c.contributionAmount = new BN(c.contributionAmount, base);
|
||||
return c;
|
||||
});
|
||||
return crowdFund;
|
||||
}
|
||||
|
||||
export function formatProposalFromGet(proposal: ProposalWithCrowdFund) {
|
||||
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
||||
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
||||
proposal.crowdFund = formatCrowdFundFromGet(proposal.crowdFund);
|
||||
for (let i = 0; i < proposal.crowdFund.milestones.length; i++) {
|
||||
proposal.milestones[i] = {
|
||||
...proposal.milestones[i],
|
||||
...proposal.crowdFund.milestones[i],
|
||||
};
|
||||
}
|
||||
return proposal;
|
||||
}
|
||||
|
||||
// TODO: i18n on case-by-case basis
|
||||
export function generateProposalUrl(id: number, title: string) {
|
||||
const slug = title
|
||||
|
@ -46,3 +80,32 @@ export function extractProposalIdFromUrl(slug: string) {
|
|||
}
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
// pre-hydration massage (BNify JSONed BNs)
|
||||
export function massageSerializedState(state: AppState) {
|
||||
// proposals
|
||||
state.proposal.proposals.forEach(p => {
|
||||
formatCrowdFundFromGet(p.crowdFund, 16);
|
||||
for (let i = 0; i < p.crowdFund.milestones.length; i++) {
|
||||
p.milestones[i] = {
|
||||
...p.milestones[i],
|
||||
...p.crowdFund.milestones[i],
|
||||
};
|
||||
}
|
||||
});
|
||||
// users
|
||||
const bnUserProp = (p: UserProposal) => {
|
||||
p.funded = new BN(p.funded, 16);
|
||||
p.target = new BN(p.target, 16);
|
||||
return p;
|
||||
};
|
||||
Object.values(state.users.map).forEach(user => {
|
||||
user.createdProposals.forEach(bnUserProp);
|
||||
user.fundedProposals.forEach(bnUserProp);
|
||||
user.comments.forEach(c => {
|
||||
c.proposal = bnUserProp(c.proposal);
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
export function getBase64(img: Blob, callback: (base64: string) => void) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result as string));
|
||||
reader.readAsDataURL(img);
|
||||
}
|
||||
|
||||
export function dataUrlToBlob(dataUrl: string) {
|
||||
const base64ImageContent = dataUrl.replace(/^data:[a-z]+\/[a-z]+;base64,/, '');
|
||||
const mimeSearch = dataUrl.match(/^data:([a-z]+\/[a-z]+)(;base64,)/);
|
||||
if (!mimeSearch || mimeSearch.length !== 3) {
|
||||
throw new Error(
|
||||
'dataUrlToBlob could not find mime type, or base64 was missing: ' +
|
||||
dataUrl.substring(0, 200),
|
||||
);
|
||||
} else {
|
||||
return base64ToBlob(base64ImageContent, mimeSearch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export function base64ToBlob(base64: string, mime: string) {
|
||||
mime = mime || '';
|
||||
const sliceSize = 1024;
|
||||
const byteChars = window.atob(base64);
|
||||
const byteArrays = [];
|
||||
for (let offset = 0, len = byteChars.length; offset < len; offset += sliceSize) {
|
||||
const slice = byteChars.slice(offset, offset + sliceSize);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
return new Blob(byteArrays, { type: mime });
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
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')];
|
||||
|
||||
|
@ -10,29 +13,51 @@ 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];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!process.env.PUBLIC_HOST_URL) {
|
||||
const envProductionRequiredHandler = (envVariable, fallbackValue) => {
|
||||
if (!process.env[envVariable]) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error(
|
||||
'The process.env.PUBLIC_HOST_URL environment variable is required but was not specified.',
|
||||
`The process.env.${envVariable} environment variable is required but was not specified.`,
|
||||
);
|
||||
}
|
||||
process.env.PUBLIC_HOST_URL = 'http://localhost:' + (process.env.PORT || 3000);
|
||||
process.env[envVariable] = fallbackValue;
|
||||
}
|
||||
};
|
||||
|
||||
envProductionRequiredHandler(
|
||||
'PUBLIC_HOST_URL',
|
||||
'http://localhost:' + (process.env.PORT || 3000),
|
||||
);
|
||||
envProductionRequiredHandler(
|
||||
'CROWD_FUND_URL',
|
||||
'https://eip-712.herokuapp.com/contract/crowd-fund',
|
||||
);
|
||||
envProductionRequiredHandler(
|
||||
'CROWD_FUND_FACTORY_URL',
|
||||
'https://eip-712.herokuapp.com/contract/factory',
|
||||
);
|
||||
|
||||
if (!process.env.BACKEND_URL) {
|
||||
process.env.BACKEND_URL = 'http://localhost:5000';
|
||||
}
|
||||
|
||||
if (!process.env.SENTRY_RELEASE) {
|
||||
process.env.SENTRY_RELEASE = childProcess
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
.trim();
|
||||
}
|
||||
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
|
@ -44,10 +69,14 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
|||
|
||||
module.exports = () => {
|
||||
const raw = {
|
||||
PORT: process.env.PORT || 3000,
|
||||
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',
|
||||
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:5000',
|
||||
PORT: process.env.PORT || 3000,
|
||||
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
||||
SENTRY_RELEASE: process.env.SENTRY_RELEASE,
|
||||
};
|
||||
|
||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
{
|
||||
"name": "grant",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
"tsc": "tsc",
|
||||
"link-contracts": "cd client/lib && ln -s ../../build/contracts contracts",
|
||||
"ganache": "ganache-cli -b 5",
|
||||
"ganache": "ganache-cli -b 5 -s testGrantIo -e 1000",
|
||||
"truffle": "truffle exec ./bin/init-truffle.js && cd client/lib/contracts && truffle console",
|
||||
"storybook": "start-storybook -p 9001 -c .storybook"
|
||||
},
|
||||
"engines": {
|
||||
"node": "8.13.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
|
@ -40,6 +44,8 @@
|
|||
"@babel/register": "^7.0.0",
|
||||
"@ledgerhq/hw-app-eth": "4.23.0",
|
||||
"@ledgerhq/hw-transport-u2f": "4.21.0",
|
||||
"@sentry/browser": "^4.3.2",
|
||||
"@sentry/node": "^4.3.2",
|
||||
"@svgr/webpack": "^2.4.0",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/cors": "^2.8.4",
|
||||
|
@ -54,6 +60,7 @@
|
|||
"@types/node": "^10.3.1",
|
||||
"@types/numeral": "^0.0.25",
|
||||
"@types/react": "16.4.18",
|
||||
"@types/react-cropper": "^0.10.3",
|
||||
"@types/react-dom": "16.0.9",
|
||||
"@types/react-helmet": "^5.0.7",
|
||||
"@types/react-redux": "^6.0.2",
|
||||
|
@ -82,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",
|
||||
|
@ -117,6 +123,7 @@
|
|||
"prettier-package-json": "^1.6.0",
|
||||
"query-string": "6.1.0",
|
||||
"react": "16.5.2",
|
||||
"react-cropper": "^1.0.1",
|
||||
"react-dev-utils": "^5.0.2",
|
||||
"react-dom": "16.5.2",
|
||||
"react-helmet": "^5.2.0",
|
||||
|
|
|
@ -4,10 +4,11 @@ import * as path from 'path';
|
|||
import chalk from 'chalk';
|
||||
import manifestHelpers from 'express-manifest-helpers';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import expressWinston from 'express-winston';
|
||||
import i18nMiddleware from 'i18next-express-middleware';
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
import '../config/env';
|
||||
// @ts-ignore
|
||||
import * as paths from '../config/paths';
|
||||
import log from './log';
|
||||
|
@ -17,10 +18,17 @@ import i18n from './i18n';
|
|||
process.env.SERVER_SIDE_RENDER = 'true';
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
dotenv.config();
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
release: process.env.SENTRY_RELEASE,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
// sentry
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
|
||||
// log requests
|
||||
app.use(expressWinston.logger({ winstonInstance: log }));
|
||||
|
||||
|
@ -61,6 +69,7 @@ app.use(
|
|||
|
||||
app.use(serverRender());
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
app.use(expressWinston.errorLogger({ winstonInstance: log }));
|
||||
|
||||
app.listen(process.env.PORT || 3000, () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import i18n from './i18n';
|
|||
|
||||
// @ts-ignore
|
||||
import * as paths from '../config/paths';
|
||||
import { storeActionsForPath } from './ssrAsync';
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
let cachedStats: any;
|
||||
|
@ -78,6 +79,7 @@ const chunkExtractFromLoadables = (loadableState: any) =>
|
|||
|
||||
const serverRenderer = () => async (req: Request, res: Response) => {
|
||||
const { store } = configureStore();
|
||||
await storeActionsForPath(req.url, store);
|
||||
|
||||
// i18n
|
||||
const locale = (req as any).language;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { Store } from 'redux';
|
||||
import { fetchProposal } from 'modules/proposals/actions';
|
||||
import {
|
||||
fetchUser,
|
||||
fetchUserCreated,
|
||||
fetchUserFunded,
|
||||
fetchUserComments,
|
||||
} from 'modules/users/actions';
|
||||
import { extractProposalIdFromUrl } from 'utils/api';
|
||||
|
||||
const pathActions = [
|
||||
{
|
||||
matcher: /^\/proposals\/(.+)$/,
|
||||
action: (match: RegExpMatchArray, store: Store) => {
|
||||
const proposalId = extractProposalIdFromUrl(match[1]);
|
||||
if (proposalId) {
|
||||
return store.dispatch<any>(fetchProposal(proposalId));
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: /^\/profile\/(.+)$/,
|
||||
action: (match: RegExpMatchArray, store: Store) => {
|
||||
const userId = match[1];
|
||||
if (userId) {
|
||||
return Promise.all([
|
||||
store.dispatch<any>(fetchUser(userId)),
|
||||
store.dispatch<any>(fetchUserCreated(userId)),
|
||||
store.dispatch<any>(fetchUserFunded(userId)),
|
||||
store.dispatch<any>(fetchUserComments(userId)),
|
||||
]);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function storeActionsForPath(path: string, store: Store) {
|
||||
const pathAction = pathActions.find(pa => !!path.match(pa.matcher));
|
||||
if (pathAction) {
|
||||
const matches = path.match(pathAction.matcher);
|
||||
if (matches) {
|
||||
return pathAction.action(matches, store);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
|
@ -212,13 +212,13 @@ export function getProposalWithCrowdFund({
|
|||
amountVotingForRefund: new BN(0),
|
||||
percentVotingForRefund: 0,
|
||||
},
|
||||
crowdFundContract: {},
|
||||
};
|
||||
|
||||
const props = {
|
||||
sendLoading: false,
|
||||
fundCrowdFund,
|
||||
web3: new Web3(),
|
||||
isMissingWeb3: false,
|
||||
proposal,
|
||||
...proposal, // yeah...
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export interface Contribution {
|
||||
id: string;
|
||||
txId: string;
|
||||
proposalId: number;
|
||||
userId: number;
|
||||
fromAddress: string;
|
||||
amount: string;
|
||||
dateCreated: number;
|
||||
}
|
|
@ -2,6 +2,7 @@ export * from './user';
|
|||
export * from './social';
|
||||
export * from './create';
|
||||
export * from './comment';
|
||||
export * from './contribution';
|
||||
export * from './milestone';
|
||||
export * from './update';
|
||||
export * from './proposal';
|
||||
|
|
|
@ -46,7 +46,6 @@ export interface Proposal {
|
|||
|
||||
export interface ProposalWithCrowdFund extends Proposal {
|
||||
crowdFund: CrowdFund;
|
||||
crowdFundContract: any;
|
||||
}
|
||||
|
||||
export interface ProposalComments {
|
||||
|
|
|
@ -1612,6 +1612,60 @@
|
|||
dependencies:
|
||||
any-observable "^0.3.0"
|
||||
|
||||
"@sentry/browser@^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-4.3.2.tgz#430b83583c5c25d33041dd80bf6ed19216086f70"
|
||||
dependencies:
|
||||
"@sentry/core" "4.3.2"
|
||||
"@sentry/types" "4.3.2"
|
||||
"@sentry/utils" "4.3.2"
|
||||
|
||||
"@sentry/core@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.3.2.tgz#e8a2850a11316d865ed7d3030ee2c4a1608bb1d8"
|
||||
dependencies:
|
||||
"@sentry/hub" "4.3.2"
|
||||
"@sentry/minimal" "4.3.2"
|
||||
"@sentry/types" "4.3.2"
|
||||
"@sentry/utils" "4.3.2"
|
||||
|
||||
"@sentry/hub@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.3.2.tgz#1ea10038e2080035d2bc09f5f26829cd106a1516"
|
||||
dependencies:
|
||||
"@sentry/types" "4.3.2"
|
||||
"@sentry/utils" "4.3.2"
|
||||
|
||||
"@sentry/minimal@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.3.2.tgz#c0958e5858b2105a6a0b523787e459a03af603cc"
|
||||
dependencies:
|
||||
"@sentry/hub" "4.3.2"
|
||||
"@sentry/types" "4.3.2"
|
||||
|
||||
"@sentry/node@^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.3.2.tgz#3c7cd3aff238f3b1eb3252147b963cbdf520aa45"
|
||||
dependencies:
|
||||
"@sentry/core" "4.3.2"
|
||||
"@sentry/hub" "4.3.2"
|
||||
"@sentry/types" "4.3.2"
|
||||
"@sentry/utils" "4.3.2"
|
||||
cookie "0.3.1"
|
||||
lsmod "1.0.0"
|
||||
md5 "2.2.1"
|
||||
stack-trace "0.0.10"
|
||||
|
||||
"@sentry/types@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.3.2.tgz#28b143979482fcbc9f9e520250482dde015b13fa"
|
||||
|
||||
"@sentry/utils@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.3.2.tgz#de14046eba972af9d62508f78cd998b0352d634a"
|
||||
dependencies:
|
||||
"@sentry/types" "4.3.2"
|
||||
|
||||
"@storybook/addons@4.0.0-alpha.22":
|
||||
version "4.0.0-alpha.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.0.0-alpha.22.tgz#08d89396fff216c0d5aa305f7ac851b6bc34b6cf"
|
||||
|
@ -1858,6 +1912,10 @@
|
|||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/cropperjs@*":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/cropperjs/-/cropperjs-1.1.3.tgz#8b2264fb45e933c3eda149cbd08a4f1926dfd8e2"
|
||||
|
||||
"@types/dotenv@^4.0.3":
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67"
|
||||
|
@ -1967,6 +2025,13 @@
|
|||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d"
|
||||
|
||||
"@types/react-cropper@^0.10.3":
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-cropper/-/react-cropper-0.10.3.tgz#d7ca18667d9cdad9469d3de6469104924d8217d5"
|
||||
dependencies:
|
||||
"@types/cropperjs" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@16.0.9":
|
||||
version "16.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.9.tgz#73ceb7abe6703822eab6600e65c5c52efd07fb91"
|
||||
|
@ -4094,6 +4159,10 @@ chardet@^0.7.0:
|
|||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
|
||||
charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
|
||||
check-error@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
|
@ -4765,12 +4834,9 @@ create-react-context@0.2.3:
|
|||
fbjs "^0.8.0"
|
||||
gud "^1.0.0"
|
||||
|
||||
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"
|
||||
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-spawn@5.1.0, cross-spawn@^5.0.1:
|
||||
version "5.1.0"
|
||||
|
@ -4797,6 +4863,10 @@ cross-spawn@^3.0.0:
|
|||
lru-cache "^4.0.1"
|
||||
which "^1.2.9"
|
||||
|
||||
crypt@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||
|
||||
cryptiles@3.x.x:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
|
||||
|
@ -8067,7 +8137,7 @@ is-boolean-object@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
|
||||
|
||||
is-buffer@^1.0.2, is-buffer@^1.1.5:
|
||||
is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
||||
|
@ -8382,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"
|
||||
|
||||
|
@ -9643,6 +9713,10 @@ lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
|
|||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
lsmod@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b"
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
|
||||
|
@ -9736,6 +9810,14 @@ md5.js@^1.3.4:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
md5@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
|
||||
dependencies:
|
||||
charenc "~0.0.1"
|
||||
crypt "~0.0.1"
|
||||
is-buffer "~1.1.1"
|
||||
|
||||
mdn-data@~1.1.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
|
||||
|
@ -12512,6 +12594,13 @@ react-copy-to-clipboard@^5.0.1:
|
|||
copy-to-clipboard "^3"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-cropper@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-cropper/-/react-cropper-1.0.1.tgz#6e5595abb8576088ab3e51ecfdcfe5f865d0340f"
|
||||
dependencies:
|
||||
cropperjs v1.0.0-rc.3
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-dev-utils@6.0.0-next.3e165448:
|
||||
version "6.0.0-next.3e165448"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.0-next.3e165448.tgz#d573ed0ba692f6cee23166f99204e5761df0897c"
|
||||
|
@ -14208,7 +14297,7 @@ stable@~0.1.6:
|
|||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
|
||||
stack-trace@0.0.x:
|
||||
stack-trace@0.0.10, stack-trace@0.0.x:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
|
||||
|
|
Loading…
Reference in New Issue