Address TODOs (#349)
* todos: simple ones, removals & modifications to NOTE * rem-todo: reduced markdown images are removed by sanitizer * be todo: add user validation to create * be todo: improve test_invide_api tests * be todo: remove todo comment * fe todo: set error messages on reducers * fe todo: upgrade and enable react-helmet * todos - remove uneeded * fe todos: remove unecessary * be: fix remaining staking contribution calculation
This commit is contained in:
parent
25a71ced1b
commit
1ae519e251
|
@ -401,7 +401,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
||||||
<Markdown source={p.content} />
|
<Markdown source={p.content} />
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
|
||||||
{/* TODO - comments, milestones, updates &etc. */}
|
|
||||||
<Collapse.Panel key="json" header="json">
|
<Collapse.Panel key="json" header="json">
|
||||||
<pre>{JSON.stringify(p, null, 4)}</pre>
|
<pre>{JSON.stringify(p, null, 4)}</pre>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
@ -469,8 +468,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* TODO: contributors here? */}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,6 @@ class Milestone(db.Model):
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
payout_percent = db.Column(db.String(255), nullable=False)
|
payout_percent = db.Column(db.String(255), nullable=False)
|
||||||
immediate_payout = db.Column(db.Boolean)
|
immediate_payout = db.Column(db.Boolean)
|
||||||
# TODO: change to estimated_duration (sec or ms) -- FE can calc from dates on draft
|
|
||||||
date_estimated = db.Column(db.DateTime, nullable=False)
|
date_estimated = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
stage = db.Column(db.String(255), nullable=False)
|
stage = db.Column(db.String(255), nullable=False)
|
||||||
|
|
|
@ -376,14 +376,14 @@ class Proposal(db.Model):
|
||||||
|
|
||||||
def get_staking_contribution(self, user_id: int):
|
def get_staking_contribution(self, user_id: int):
|
||||||
contribution = None
|
contribution = None
|
||||||
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.contributed)
|
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.amount_staked)
|
||||||
# check funding
|
# check funding
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
# find pending contribution for any user of remaining amount
|
# find pending contribution for any user of remaining amount
|
||||||
# TODO: Filter by staking=True?
|
|
||||||
contribution = ProposalContribution.query.filter_by(
|
contribution = ProposalContribution.query.filter_by(
|
||||||
proposal_id=self.id,
|
proposal_id=self.id,
|
||||||
status=ProposalStatus.PENDING,
|
status=ProposalStatus.PENDING,
|
||||||
|
staking=True,
|
||||||
).first()
|
).first()
|
||||||
if not contribution:
|
if not contribution:
|
||||||
contribution = self.create_contribution(
|
contribution = self.create_contribution(
|
||||||
|
@ -537,6 +537,14 @@ class Proposal(db.Model):
|
||||||
funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
||||||
return str(funded)
|
return str(funded)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def amount_staked(self):
|
||||||
|
contributions = ProposalContribution.query \
|
||||||
|
.filter_by(proposal_id=self.id, status=ContributionStatus.CONFIRMED, staking=True) \
|
||||||
|
.all()
|
||||||
|
amount = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
||||||
|
return str(amount)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def funded(self):
|
def funded(self):
|
||||||
target = Decimal(self.target)
|
target = Decimal(self.target)
|
||||||
|
@ -716,9 +724,6 @@ proposal_team_invite_schema = ProposalTeamInviteSchema()
|
||||||
proposal_team_invites_schema = ProposalTeamInviteSchema(many=True)
|
proposal_team_invites_schema = ProposalTeamInviteSchema(many=True)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Find a way to extend ProposalTeamInviteSchema instead of redefining
|
|
||||||
|
|
||||||
|
|
||||||
class InviteWithProposalSchema(ma.Schema):
|
class InviteWithProposalSchema(ma.Schema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProposalTeamInvite
|
model = ProposalTeamInvite
|
||||||
|
@ -768,7 +773,7 @@ class ProposalContributionSchema(ma.Schema):
|
||||||
|
|
||||||
def get_addresses(self, obj):
|
def get_addresses(self, obj):
|
||||||
# Omit 'memo' and 'sprout' for now
|
# Omit 'memo' and 'sprout' for now
|
||||||
# TODO: Add back in 'sapling' when ready
|
# NOTE: Add back in 'sapling' when ready
|
||||||
addresses = blockchain_get('/contribution/addresses', {'contributionId': obj.id})
|
addresses = blockchain_get('/contribution/addresses', {'contributionId': obj.id})
|
||||||
return {
|
return {
|
||||||
'transparent': addresses['transparent'],
|
'transparent': addresses['transparent'],
|
||||||
|
|
|
@ -4,6 +4,7 @@ from decimal import Decimal
|
||||||
from flask import Blueprint, g, request, current_app
|
from flask import Blueprint, g, request, current_app
|
||||||
from marshmallow import fields, validate
|
from marshmallow import fields, validate
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
from sentry_sdk import capture_message
|
||||||
|
|
||||||
from grant.extensions import limiter
|
from grant.extensions import limiter
|
||||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||||
|
@ -136,7 +137,7 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
dumped_comment = comment_schema.dump(comment)
|
dumped_comment = comment_schema.dump(comment)
|
||||||
|
|
||||||
# TODO: Email proposal team if top-level comment
|
# Email proposal team if top-level comment
|
||||||
if not parent:
|
if not parent:
|
||||||
for member in proposal.team:
|
for member in proposal.team:
|
||||||
send_email(member.email_address, 'proposal_comment', {
|
send_email(member.email_address, 'proposal_comment', {
|
||||||
|
@ -407,7 +408,6 @@ def post_proposal_team_invite(proposal_id, address):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Send email
|
# Send email
|
||||||
# TODO: Move this to some background task / after request action
|
|
||||||
email = address
|
email = address
|
||||||
user = User.get_by_email(email_address=address)
|
user = User.get_by_email(email_address=address)
|
||||||
if user:
|
if user:
|
||||||
|
@ -531,8 +531,9 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
id=contribution_id).first()
|
id=contribution_id).first()
|
||||||
|
|
||||||
if not contribution:
|
if not contribution:
|
||||||
# TODO: Log in sentry
|
msg = f'Unknown contribution {contribution_id} confirmed with txid {txid}, amount {amount}'
|
||||||
current_app.logger.warn(f'Unknown contribution {contribution_id} confirmed with txid {txid}')
|
capture_message(msg)
|
||||||
|
current_app.logger.warn(msg)
|
||||||
return {"message": "No contribution matching id"}, 404
|
return {"message": "No contribution matching id"}, 404
|
||||||
|
|
||||||
if contribution.status == ContributionStatus.CONFIRMED:
|
if contribution.status == ContributionStatus.CONFIRMED:
|
||||||
|
@ -578,8 +579,6 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
|
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
|
||||||
})
|
})
|
||||||
|
|
||||||
# TODO: Once we have a task queuer in place, queue emails to everyone
|
|
||||||
|
|
||||||
# on funding target reached.
|
# on funding target reached.
|
||||||
contribution.proposal.set_funded_when_ready()
|
contribution.proposal.set_funded_when_ready()
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ class RFP(db.Model):
|
||||||
matching: bool = False,
|
matching: bool = False,
|
||||||
status: str = RFPStatus.DRAFT,
|
status: str = RFPStatus.DRAFT,
|
||||||
):
|
):
|
||||||
# TODO add status assert
|
assert RFPStatus.includes(status)
|
||||||
assert Category.includes(category)
|
assert Category.includes(category)
|
||||||
self.id = gen_random_id(RFP)
|
self.id = gen_random_id(RFP)
|
||||||
self.date_created = datetime.now()
|
self.date_created = datetime.now()
|
||||||
|
|
|
@ -30,7 +30,6 @@ class ProposalReminder:
|
||||||
assert task.job_type == 1, "Job type: {} is incorrect for ProposalReminder".format(task.job_type)
|
assert task.job_type == 1, "Job type: {} is incorrect for ProposalReminder".format(task.job_type)
|
||||||
from grant.proposal.models import Proposal
|
from grant.proposal.models import Proposal
|
||||||
proposal = Proposal.query.filter_by(id=task.blob["proposal_id"]).first()
|
proposal = Proposal.query.filter_by(id=task.blob["proposal_id"]).first()
|
||||||
# TODO - replace with email
|
|
||||||
print(proposal)
|
print(proposal)
|
||||||
task.completed = True
|
task.completed = True
|
||||||
db.session.add(task)
|
db.session.add(task)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from flask_security import UserMixin, RoleMixin
|
from flask_security import UserMixin, RoleMixin
|
||||||
from flask_security.core import current_user
|
from flask_security.core import current_user
|
||||||
from flask_security.utils import hash_password, verify_and_update_password, login_user
|
from flask_security.utils import hash_password, verify_and_update_password, login_user
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from grant.comment.models import Comment
|
from grant.comment.models import Comment
|
||||||
from grant.email.models import EmailVerification, EmailRecovery
|
from grant.email.models import EmailVerification, EmailRecovery
|
||||||
from grant.email.send import send_email
|
from grant.email.send import send_email
|
||||||
|
@ -10,11 +11,11 @@ from grant.email.subscription_settings import (
|
||||||
email_subscriptions_to_dict
|
email_subscriptions_to_dict
|
||||||
)
|
)
|
||||||
from grant.extensions import ma, db, security
|
from grant.extensions import ma, db, security
|
||||||
from grant.utils.misc import make_url, gen_random_id
|
from grant.utils.misc import make_url, gen_random_id, is_email
|
||||||
from grant.utils.social import generate_social_url
|
from grant.utils.social import generate_social_url
|
||||||
from grant.utils.upload import extract_avatar_filename, construct_avatar_url
|
from grant.utils.upload import extract_avatar_filename, construct_avatar_url
|
||||||
from grant.utils import totp_2fa
|
from grant.utils import totp_2fa
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from grant.utils.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
def is_current_authed_user_id(user_id):
|
def is_current_authed_user_id(user_id):
|
||||||
|
@ -133,8 +134,6 @@ class User(db.Model, UserMixin):
|
||||||
backref=db.backref('users', lazy='dynamic'))
|
backref=db.backref('users', lazy='dynamic'))
|
||||||
arbiter_proposals = db.relationship("ProposalArbiter", lazy=True, back_populates="user")
|
arbiter_proposals = db.relationship("ProposalArbiter", lazy=True, back_populates="user")
|
||||||
|
|
||||||
# TODO - add create and validate methods
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
email_address,
|
email_address,
|
||||||
|
@ -150,6 +149,22 @@ class User(db.Model, UserMixin):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(user):
|
||||||
|
em = user.get('email_address')
|
||||||
|
if not em:
|
||||||
|
raise ValidationException('Must have email address')
|
||||||
|
if not is_email(em):
|
||||||
|
raise ValidationException('Email address looks invalid')
|
||||||
|
|
||||||
|
t = user.get('title')
|
||||||
|
if t and len(t) > 255:
|
||||||
|
raise ValidationException('Title is too long')
|
||||||
|
|
||||||
|
dn = user.get('display_name')
|
||||||
|
if dn and len(dn) > 255:
|
||||||
|
raise ValidationException('Display name is too long')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(email_address=None, password=None, display_name=None, title=None, _send_email=True):
|
def create(email_address=None, password=None, display_name=None, title=None, _send_email=True):
|
||||||
user = security.datastore.create_user(
|
user = security.datastore.create_user(
|
||||||
|
@ -158,6 +173,7 @@ class User(db.Model, UserMixin):
|
||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
title=title
|
title=title
|
||||||
)
|
)
|
||||||
|
User.validate(vars(user))
|
||||||
security.datastore.commit()
|
security.datastore.commit()
|
||||||
|
|
||||||
# user settings
|
# user settings
|
||||||
|
@ -249,7 +265,6 @@ class User(db.Model, UserMixin):
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
def set_admin(self, is_admin: bool):
|
def set_admin(self, is_admin: bool):
|
||||||
# TODO: audit entry & possibly email user
|
|
||||||
self.is_admin = is_admin
|
self.is_admin = is_admin
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
|
@ -245,10 +245,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
def test_update_proposal(self):
|
def test_update_proposal(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
# set to 1 (on)
|
# set to 1 (on)
|
||||||
resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
|
resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}",
|
||||||
|
data=json.dumps({"contributionMatching": 1}))
|
||||||
self.assert200(resp_on)
|
self.assert200(resp_on)
|
||||||
self.assertEqual(resp_on.json['contributionMatching'], 1)
|
self.assertEqual(resp_on.json['contributionMatching'], 1)
|
||||||
resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 0}))
|
resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}",
|
||||||
|
data=json.dumps({"contributionMatching": 0}))
|
||||||
self.assert200(resp_off)
|
self.assert200(resp_off)
|
||||||
self.assertEqual(resp_off.json['contributionMatching'], 0)
|
self.assertEqual(resp_off.json['contributionMatching'], 0)
|
||||||
|
|
||||||
|
@ -307,7 +309,6 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
self.assert200(resp)
|
self.assert200(resp)
|
||||||
# TODO - more tests
|
|
||||||
|
|
||||||
def test_create_rfp_succeeds(self):
|
def test_create_rfp_succeeds(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
|
@ -315,12 +316,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
"/api/v1/admin/rfps",
|
"/api/v1/admin/rfps",
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
"brief": "Some brief",
|
"brief": "Some brief",
|
||||||
"category": "CORE_DEV",
|
"category": "CORE_DEV",
|
||||||
"content": "CONTENT",
|
"content": "CONTENT",
|
||||||
"dateCloses": 1553980004,
|
"dateCloses": 1553980004,
|
||||||
"status": "DRAFT",
|
"status": "DRAFT",
|
||||||
"title": "TITLE"
|
"title": "TITLE"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
self.assert200(resp)
|
self.assert200(resp)
|
||||||
|
@ -331,13 +332,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
"/api/v1/admin/rfps",
|
"/api/v1/admin/rfps",
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
"brief": "Some brief",
|
"brief": "Some brief",
|
||||||
"category": "NOT_CORE_DEV",
|
"category": "NOT_CORE_DEV",
|
||||||
"content": "CONTENT",
|
"content": "CONTENT",
|
||||||
"dateCloses": 1553980004,
|
"dateCloses": 1553980004,
|
||||||
"status": "DRAFT",
|
"status": "DRAFT",
|
||||||
"title": "TITLE"
|
"title": "TITLE"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
self.assert400(resp)
|
self.assert400(resp)
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,10 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
|
||||||
self.assertStatus(invites_res, 200)
|
self.assertStatus(invites_res, 200)
|
||||||
|
|
||||||
# Make sure we made the team, coach
|
# Make sure we made the team, coach
|
||||||
self.assertTrue(len(self.other_proposal.team) == 2) # TODO: More thorough check than length
|
print(self.other_proposal.team)
|
||||||
|
self.assertTrue(len(self.other_proposal.team) == 2)
|
||||||
|
team_ids = [t.id for t in self.other_proposal.team]
|
||||||
|
self.assertIn(self.user.id, team_ids, 'user should be in team')
|
||||||
|
|
||||||
def test_put_user_invite_response_reject(self):
|
def test_put_user_invite_response_reject(self):
|
||||||
invite = ProposalTeamInvite(
|
invite = ProposalTeamInvite(
|
||||||
|
@ -67,7 +70,9 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
|
||||||
self.assertStatus(invites_res, 200)
|
self.assertStatus(invites_res, 200)
|
||||||
|
|
||||||
# Make sure we made the team, coach
|
# Make sure we made the team, coach
|
||||||
self.assertTrue(len(self.other_proposal.team) == 1) # TODO: More thorough check than length
|
self.assertTrue(len(self.other_proposal.team) == 1)
|
||||||
|
team_ids = [t.id for t in self.other_proposal.team]
|
||||||
|
self.assertNotIn(self.user.id, team_ids, 'user should NOT be in team')
|
||||||
|
|
||||||
def test_no_auth_put_user_invite_response(self):
|
def test_no_auth_put_user_invite_response(self):
|
||||||
invite = ProposalTeamInvite(
|
invite = ProposalTeamInvite(
|
||||||
|
|
|
@ -144,7 +144,7 @@ class TestUserAPI(BaseUserConfig):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
|
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
|
||||||
updated_user["displayName"] = 'new display name'
|
updated_user["displayName"] = 'new display name'
|
||||||
updated_user["avatar"] = '' # TODO confirm avatar is no longer a dict
|
updated_user["avatar"] = ''
|
||||||
updated_user["socialMedias"] = []
|
updated_user["socialMedias"] = []
|
||||||
|
|
||||||
user_update_resp = self.app.put(
|
user_update_resp = self.app.put(
|
||||||
|
|
|
@ -33,7 +33,6 @@ export interface VOut {
|
||||||
scriptPubKey: ScriptPubKey;
|
scriptPubKey: ScriptPubKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
txid: string;
|
txid: string;
|
||||||
hex: string;
|
hex: string;
|
||||||
|
@ -46,7 +45,7 @@ export interface Transaction {
|
||||||
time: number;
|
time: number;
|
||||||
vin: VIn[];
|
vin: VIn[];
|
||||||
vout: VOut[];
|
vout: VOut[];
|
||||||
// TODO: fill me out, what is this?
|
// unclear what vjoinsplit is
|
||||||
vjoinsplit: any[];
|
vjoinsplit: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +73,6 @@ export interface BlockWithTransactionIds extends Block {
|
||||||
tx: string[];
|
tx: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface BlockWithTransactions extends Block {
|
export interface BlockWithTransactions extends Block {
|
||||||
tx: Transaction[];
|
tx: Transaction[];
|
||||||
}
|
}
|
||||||
|
@ -107,33 +105,44 @@ export interface ValidationResponse {
|
||||||
isvalid: boolean;
|
isvalid: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: Type all methods with signatures from
|
|
||||||
// https://github.com/zcash/zcash/blob/master/doc/payment-api.md
|
// https://github.com/zcash/zcash/blob/master/doc/payment-api.md
|
||||||
interface ZCashNode {
|
interface ZCashNode {
|
||||||
getblockchaininfo: () => Promise<BlockChainInfo>;
|
getblockchaininfo: () => Promise<BlockChainInfo>;
|
||||||
getblockcount: () => Promise<number>;
|
getblockcount: () => Promise<number>;
|
||||||
getblock: {
|
getblock: {
|
||||||
(numberOrHash: string | number, verbosity?: 1): Promise<BlockWithTransactionIds>;
|
(numberOrHash: string | number, verbosity?: 1): Promise<
|
||||||
(numberOrHash: string | number, verbosity: 2): Promise<BlockWithTransactions>;
|
BlockWithTransactionIds
|
||||||
|
>;
|
||||||
|
(numberOrHash: string | number, verbosity: 2): Promise<
|
||||||
|
BlockWithTransactions
|
||||||
|
>;
|
||||||
(numberOrHash: string | number, verbosity: 0): Promise<string>;
|
(numberOrHash: string | number, verbosity: 0): Promise<string>;
|
||||||
}
|
};
|
||||||
gettransaction: (txid: string) => Promise<Transaction>;
|
gettransaction: (txid: string) => Promise<Transaction>;
|
||||||
validateaddress: (address: string) => Promise<ValidationResponse>;
|
validateaddress: (address: string) => Promise<ValidationResponse>;
|
||||||
z_getbalance: (address: string, minConf?: number) => Promise<number>;
|
z_getbalance: (address: string, minConf?: number) => Promise<number>;
|
||||||
z_getnewaddress: (type?: 'sprout' | 'sapling') => Promise<string>;
|
z_getnewaddress: (type?: "sprout" | "sapling") => Promise<string>;
|
||||||
z_listaddresses: () => Promise<string[]>;
|
z_listaddresses: () => Promise<string[]>;
|
||||||
z_listreceivedbyaddress: (address: string, minConf?: number) => Promise<Receipt[]>;
|
z_listreceivedbyaddress: (
|
||||||
z_importviewingkey: (key: string, rescan?: 'yes' | 'no' | 'whenkeyisnew', startHeight?: number) => Promise<void>;
|
address: string,
|
||||||
|
minConf?: number
|
||||||
|
) => Promise<Receipt[]>;
|
||||||
|
z_importviewingkey: (
|
||||||
|
key: string,
|
||||||
|
rescan?: "yes" | "no" | "whenkeyisnew",
|
||||||
|
startHeight?: number
|
||||||
|
) => Promise<void>;
|
||||||
z_exportviewingkey: (zaddr: string) => Promise<string>;
|
z_exportviewingkey: (zaddr: string) => Promise<string>;
|
||||||
z_validatepaymentdisclosure: (disclosure: string) => Promise<DisclosedPayment>;
|
z_validatepaymentdisclosure: (
|
||||||
|
disclosure: string
|
||||||
|
) => Promise<DisclosedPayment>;
|
||||||
z_validateaddress: (address: string) => Promise<ValidationResponse>;
|
z_validateaddress: (address: string) => Promise<ValidationResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rpcOptions = {
|
export const rpcOptions = {
|
||||||
url: env.ZCASH_NODE_URL,
|
url: env.ZCASH_NODE_URL,
|
||||||
username: env.ZCASH_NODE_USERNAME,
|
username: env.ZCASH_NODE_USERNAME,
|
||||||
password: env.ZCASH_NODE_PASSWORD,
|
password: env.ZCASH_NODE_PASSWORD
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ZCashNode = stdrpc(rpcOptions);
|
const node: ZCashNode = stdrpc(rpcOptions);
|
||||||
|
@ -152,28 +161,31 @@ export async function initNode() {
|
||||||
}
|
}
|
||||||
if (info.chain.includes("test")) {
|
if (info.chain.includes("test")) {
|
||||||
network = bitcore.Networks.testnet;
|
network = bitcore.Networks.testnet;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
network = bitcore.Networks.mainnet;
|
network = bitcore.Networks.mainnet;
|
||||||
}
|
}
|
||||||
}
|
} catch (err) {
|
||||||
catch(err) {
|
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error(err.response ? err.response.data : err);
|
log.error(err.response ? err.response.data : err);
|
||||||
log.error('Failed to connect to zcash node with the following credentials:\r\n', rpcOptions);
|
log.error(
|
||||||
|
"Failed to connect to zcash node with the following credentials:\r\n",
|
||||||
|
rpcOptions
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sprout address is readable
|
// Check if sprout address is readable
|
||||||
try {
|
try {
|
||||||
if (!env.SPROUT_ADDRESS) {
|
if (!env.SPROUT_ADDRESS) {
|
||||||
console.error('Missing SPROUT_ADDRESS environment variable, exiting');
|
console.error("Missing SPROUT_ADDRESS environment variable, exiting");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (!env.SPROUT_VIEWKEY) {
|
if (!env.SPROUT_VIEWKEY) {
|
||||||
log.error('Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting');
|
log.error(
|
||||||
|
"Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting"
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
|
await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
|
||||||
|
@ -183,7 +195,7 @@ export async function initNode() {
|
||||||
|
|
||||||
export function getNetwork() {
|
export function getNetwork() {
|
||||||
if (!network) {
|
if (!network) {
|
||||||
throw new Error('Called getNetwork before initNode');
|
throw new Error("Called getNetwork before initNode");
|
||||||
}
|
}
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
@ -194,11 +206,15 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
|
||||||
try {
|
try {
|
||||||
const tx = await node.gettransaction(txid);
|
const tx = await node.gettransaction(txid);
|
||||||
const block = await node.getblock(tx.blockhash);
|
const block = await node.getblock(tx.blockhash);
|
||||||
const height = block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
const height =
|
||||||
|
block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
||||||
return height.toString();
|
return height.toString();
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
console.warn(`Attempted to get block height for tx ${txid} but failed with the following error:\n`, err);
|
console.warn(
|
||||||
console.warn('Falling back to hard-coded starter blocks');
|
`Attempted to get block height for tx ${txid} but failed with the following error:\n`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
console.warn("Falling back to hard-coded starter blocks");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,11 +223,10 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
|
||||||
const net = getNetwork();
|
const net = getNetwork();
|
||||||
if (net === bitcore.Networks.mainnet) {
|
if (net === bitcore.Networks.mainnet) {
|
||||||
return env.MAINNET_START_BLOCK;
|
return env.MAINNET_START_BLOCK;
|
||||||
}
|
} else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
||||||
else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
|
||||||
return env.TESTNET_START_BLOCK;
|
return env.TESTNET_START_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regtest or otherwise unknown networks should start at the bottom
|
// Regtest or otherwise unknown networks should start at the bottom
|
||||||
return '0';
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,7 @@ export function authenticate(secret: string) {
|
||||||
return hash === sha256(secret);
|
return hash === sha256(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Not fully confident in compatibility with most bip32 wallets,
|
// NOTE: this is just one way to derive t-addrs
|
||||||
// do more work to ensure this is reliable.
|
|
||||||
export function deriveTransparentAddress(index: number, network: any) {
|
export function deriveTransparentAddress(index: number, network: any) {
|
||||||
const root = new HDPublicKey(env.BIP32_XPUB);
|
const root = new HDPublicKey(env.BIP32_XPUB);
|
||||||
const child = root.derive(`m/0/${index}`);
|
const child = root.derive(`m/0/${index}`);
|
||||||
|
@ -39,14 +38,16 @@ export function removeItem<T>(arr: T[], remove: T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeHexMemo(memo: string) {
|
export function encodeHexMemo(memo: string) {
|
||||||
return new Buffer(memo, 'utf8').toString('hex');
|
return new Buffer(memo, "utf8").toString("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeHexMemo(memoHex: string) {
|
export function decodeHexMemo(memoHex: string) {
|
||||||
return new Buffer(memoHex, 'hex')
|
return (
|
||||||
.toString()
|
new Buffer(memoHex, "hex")
|
||||||
// Remove null bytes from zero padding
|
.toString()
|
||||||
.replace(/\0.*$/g, '');
|
// Remove null bytes from zero padding
|
||||||
|
.replace(/\0.*$/g, "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeContributionMemo(contributionId: number) {
|
export function makeContributionMemo(contributionId: number) {
|
||||||
|
@ -54,14 +55,15 @@ export function makeContributionMemo(contributionId: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContributionIdFromMemo(memoHex: string) {
|
export function getContributionIdFromMemo(memoHex: string) {
|
||||||
const matches = decodeHexMemo(memoHex).match(/Contribution ([0-9]+) on Grant\.io/);
|
const matches = decodeHexMemo(memoHex).match(
|
||||||
|
/Contribution ([0-9]+) on Grant\.io/
|
||||||
|
);
|
||||||
if (matches && matches[1]) {
|
if (matches && matches[1]) {
|
||||||
return parseInt(matches[1], 10);
|
return parseInt(matches[1], 10);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this more robust
|
|
||||||
export function toBaseUnit(unit: number) {
|
export function toBaseUnit(unit: number) {
|
||||||
return Math.floor(100000000 * unit);
|
return Math.floor(100000000 * unit);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,4 @@ Tests can be found in `cypress/integration`. Cypress will hot-reload open tests
|
||||||
|
|
||||||
### CI
|
### CI
|
||||||
|
|
||||||
TODO
|
Coming soon.
|
||||||
|
|
|
@ -79,7 +79,6 @@ describe("create.fund.ms2.no-vote.re-vote", () => {
|
||||||
"Request milestone payout",
|
"Request milestone payout",
|
||||||
{ timeout: 20000 }
|
{ timeout: 20000 }
|
||||||
).click();
|
).click();
|
||||||
// TODO: fix this bug (the following fails)
|
|
||||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||||
timeout: 20000
|
timeout: 20000
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,6 @@ class BasicHead extends React.Component<Props> {
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="Zcash, Zcash Foundation, Zcash Foundation Grants, Zcash Grants, Zcash Grant, ZF Grants, ZFGrants"
|
content="Zcash, Zcash Foundation, Zcash Foundation Grants, Zcash Grants, Zcash Grant, ZF Grants, ZFGrants"
|
||||||
/>
|
/>
|
||||||
<meta name={`${title} page`} content={`${title} page stuff`} />
|
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
|
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
|
||||||
|
@ -41,7 +40,6 @@ class BasicHead extends React.Component<Props> {
|
||||||
<meta property="og:url" content={defaultOgpUrl} />
|
<meta property="og:url" content={defaultOgpUrl} />
|
||||||
<meta property="og:image" content={defaultOgpImage} />
|
<meta property="og:image" content={defaultOgpImage} />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
{/* TODO: i18n */}
|
|
||||||
{/* <meta property="og:locale:alternate" content="en_US" /> */}
|
{/* <meta property="og:locale:alternate" content="en_US" /> */}
|
||||||
{/* <meta property="og:locale:alternate" content="de_DE" /> */}
|
{/* <meta property="og:locale:alternate" content="de_DE" /> */}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,6 @@ class Comment extends React.Component<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
// TODO: Come up with better check on if our comment post was a success
|
|
||||||
const { isPostCommentPending, postCommentError } = this.props;
|
const { isPostCommentPending, postCommentError } = this.props;
|
||||||
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
||||||
this.setState({ reply: '', isReplying: false });
|
this.setState({ reply: '', isReplying: false });
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
// import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
// import { urlToPublic } from 'utils/helpers';
|
import { urlToPublic } from 'utils/helpers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -12,25 +12,22 @@ interface Props {
|
||||||
|
|
||||||
export default class HeaderDetails extends React.Component<Props> {
|
export default class HeaderDetails extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
// TODO: Uncomment once helmet is fixed
|
const { title, image, url, type, description } = this.props;
|
||||||
// https://github.com/nfl/react-helmet/issues/373
|
return (
|
||||||
return null;
|
<Helmet>
|
||||||
// const { title, image, url, type, description } = this.props;
|
<title>{`ZF Grants - ${title}`}</title>
|
||||||
// return (
|
{/* open graph protocol */}
|
||||||
// <Helmet>
|
{type && <meta property="og:type" content="website" />}
|
||||||
// <title>{`ZF Grants - ${title}`}</title>
|
<meta property="og:title" content={title} />
|
||||||
// {/* open graph protocol */}
|
{description && <meta property="og:description" content={description} />}
|
||||||
// {type && <meta property="og:type" content="website" />}
|
{url && <meta property="og:url" content={urlToPublic(url)} />}
|
||||||
// <meta property="og:title" content={title} />
|
{image && <meta property="og:image" content={urlToPublic(image)} />}
|
||||||
// {description && <meta property="og:description" content={description} />}
|
{/* twitter card */}
|
||||||
// {url && <meta property="og:url" content={urlToPublic(url)} />}
|
<meta property="twitter:title" content={title} />
|
||||||
// {image && <meta property="og:image" content={urlToPublic(image)} />}
|
{description && <meta property="twitter:description" content={description} />}
|
||||||
// {/* twitter card */}
|
{url && <meta property="twitter:url" content={urlToPublic(url)} />}
|
||||||
// <meta property="twitter:title" content={title} />
|
{image && <meta property="twitter:image" content={urlToPublic(image)} />}
|
||||||
// {description && <meta property="twitter:description" content={description} />}
|
</Helmet>
|
||||||
// {url && <meta property="twitter:url" content={urlToPublic(url)} />}
|
);
|
||||||
// {image && <meta property="twitter:image" content={urlToPublic(image)} />}
|
|
||||||
// </Helmet>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,6 @@ class Profile extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Profile">
|
<div className="Profile">
|
||||||
{/* TODO: customize details for funders/creators */}
|
|
||||||
<HeaderDetails
|
<HeaderDetails
|
||||||
title={`${user.displayName} is funding projects on ZF Grants`}
|
title={`${user.displayName} is funding projects on ZF Grants`}
|
||||||
description={`Join ${user.displayName} in funding the future!`}
|
description={`Join ${user.displayName} in funding the future!`}
|
||||||
|
|
|
@ -240,7 +240,6 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get values from proposal
|
|
||||||
const { target, funded } = this.props.proposal;
|
const { target, funded } = this.props.proposal;
|
||||||
const remainingTarget = target.sub(funded);
|
const remainingTarget = target.sub(funded);
|
||||||
const amount = parseFloat(value);
|
const amount = parseFloat(value);
|
||||||
|
|
|
@ -55,7 +55,6 @@ class ProposalComments extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
// TODO: Come up with better check on if our comment post was a success
|
|
||||||
const { isPostCommentPending, postCommentError } = this.props;
|
const { isPostCommentPending, postCommentError } = this.props;
|
||||||
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
||||||
this.setState({ comment: '' });
|
this.setState({ comment: '' });
|
||||||
|
|
|
@ -386,8 +386,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
||||||
case types.PROPOSAL_UPDATES_REJECTED:
|
case types.PROPOSAL_UPDATES_REJECTED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
// TODO: Get action to send real error
|
updatesError: (payload && payload.message) || payload.toString(),
|
||||||
updatesError: 'Failed to fetch updates',
|
|
||||||
isFetchingUpdates: false,
|
isFetchingUpdates: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -402,8 +401,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
||||||
case types.PROPOSAL_CONTRIBUTIONS_REJECTED:
|
case types.PROPOSAL_CONTRIBUTIONS_REJECTED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
// TODO: Get action to send real error
|
fetchContributionsError: (payload && payload.message) || payload.toString(),
|
||||||
fetchContributionsError: 'Failed to fetch updates',
|
|
||||||
isFetchingContributions: false,
|
isFetchingContributions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,7 @@ export default (state: RFPState = INITIAL_STATE, action: any): RFPState => {
|
||||||
case types.FETCH_RFP_REJECTED:
|
case types.FETCH_RFP_REJECTED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
// TODO: Get action to send real error
|
fetchRfpsError: (payload && payload.message) || payload.toString(),
|
||||||
fetchRfpsError: 'Failed to fetch rfps',
|
|
||||||
isFetchingRfps: false,
|
isFetchingRfps: false,
|
||||||
};
|
};
|
||||||
case types.FETCH_RFPS_FULFILLED:
|
case types.FETCH_RFPS_FULFILLED:
|
||||||
|
|
|
@ -114,7 +114,7 @@ export function formatRFPFromGet(rfp: RFP): RFP {
|
||||||
return rfp;
|
return rfp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: i18n on case-by-case basis
|
// NOTE: i18n on case-by-case basis
|
||||||
export function generateSlugUrl(id: number, title: string) {
|
export function generateSlugUrl(id: number, title: string) {
|
||||||
const slug = title
|
const slug = title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
|
@ -27,7 +27,6 @@ const converters: { [key in MARKDOWN_TYPE]: Showdown.Converter } = {
|
||||||
...sharedOptions,
|
...sharedOptions,
|
||||||
noHeaderId: true,
|
noHeaderId: true,
|
||||||
headerLevelStart: 4,
|
headerLevelStart: 4,
|
||||||
// TODO: Find a way to disable images
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
"@types/react": "16.4.18",
|
"@types/react": "16.4.18",
|
||||||
"@types/react-cropper": "^0.10.3",
|
"@types/react-cropper": "^0.10.3",
|
||||||
"@types/react-dom": "16.0.9",
|
"@types/react-dom": "16.0.9",
|
||||||
"@types/react-helmet": "^5.0.7",
|
"@types/react-helmet": "^5.0.8",
|
||||||
"@types/react-redux": "^6.0.2",
|
"@types/react-redux": "^6.0.2",
|
||||||
"@types/react-router": "4.4.3",
|
"@types/react-router": "4.4.3",
|
||||||
"@types/react-router-dom": "^4.3.1",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
|
@ -126,7 +126,7 @@
|
||||||
"react-cropper": "^1.0.1",
|
"react-cropper": "^1.0.1",
|
||||||
"react-dev-utils": "^5.0.2",
|
"react-dev-utils": "^5.0.2",
|
||||||
"react-dom": "16.5.2",
|
"react-dom": "16.5.2",
|
||||||
"react-helmet": "^5.2.0",
|
"react-helmet": "6.0.0-beta",
|
||||||
"react-hot-loader": "^4.3.8",
|
"react-hot-loader": "^4.3.8",
|
||||||
"react-i18next": "^8.3.5",
|
"react-i18next": "^8.3.5",
|
||||||
"react-mde": "7.0.4",
|
"react-mde": "7.0.4",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { ChunkExtractor } from '@loadable/server';
|
import { ChunkExtractor } from '@loadable/server';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -34,13 +34,6 @@ const HTML: React.SFC<Props> = ({
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="msapplication-TileColor" content="#fff" />
|
<meta name="msapplication-TileColor" content="#fff" />
|
||||||
<meta name="theme-color" content="#fff" />
|
<meta name="theme-color" content="#fff" />
|
||||||
{/* TODO: import from @fortawesome */}
|
|
||||||
{/* <link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
|
|
||||||
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/> */}
|
|
||||||
{/* Custom link & meta tags from webpack */}
|
{/* Custom link & meta tags from webpack */}
|
||||||
{extractor.getLinkElements()}
|
{extractor.getLinkElements()}
|
||||||
{linkTags.map((l, idx) => (
|
{linkTags.map((l, idx) => (
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface Contribution {
|
||||||
export interface ContributionWithAddresses extends Contribution {
|
export interface ContributionWithAddresses extends Contribution {
|
||||||
addresses: {
|
addresses: {
|
||||||
transparent: string;
|
transparent: string;
|
||||||
// TODO: Add sapling and memo in when ready
|
// NOTE: Add sapling and memo in when ready
|
||||||
// sprout: string;
|
// sprout: string;
|
||||||
// memo: string;
|
// memo: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SocialMedia } from 'types';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
userid: number;
|
userid: number;
|
||||||
emailAddress?: string; // TODO: Split into full user type
|
emailAddress?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1940,9 +1940,10 @@
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-helmet@^5.0.7":
|
"@types/react-helmet@^5.0.8":
|
||||||
version "5.0.7"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.7.tgz#1cae65b2c37fe54cf56f40cd388836d4619dbc51"
|
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.8.tgz#f080eea6652e44f60b4574463d238f268d81d9af"
|
||||||
|
integrity sha512-ZTr12eDAYI0yUiMx1K82EHqRYa8J1BOOLus+0gL+AkksUiIPwLE0wLiXa9FNqD8r9GXAi+yRPZImkRh1JNlTkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
@ -9601,6 +9602,11 @@ react-error-overlay@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
|
||||||
|
|
||||||
|
react-fast-compare@^2.0.2:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
react-fittext@^1.0.0:
|
react-fittext@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-fittext/-/react-fittext-1.0.0.tgz#836a1c04f9322f6c94cb69e45c66006fc42d37a5"
|
resolved "https://registry.yarnpkg.com/react-fittext/-/react-fittext-1.0.0.tgz#836a1c04f9322f6c94cb69e45c66006fc42d37a5"
|
||||||
|
@ -9617,13 +9623,14 @@ react-fuzzy@^0.5.2:
|
||||||
fuse.js "^3.0.1"
|
fuse.js "^3.0.1"
|
||||||
prop-types "^15.5.9"
|
prop-types "^15.5.9"
|
||||||
|
|
||||||
react-helmet@^5.2.0:
|
react-helmet@6.0.0-beta:
|
||||||
version "5.2.0"
|
version "6.0.0-beta"
|
||||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.0.tgz#a81811df21313a6d55c5f058c4aeba5d6f3d97a7"
|
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0-beta.tgz#1f2ac04521951486e4fce3296d0c88aae8cabd5c"
|
||||||
|
integrity sha512-GnNWsokebTe7fe8MH2I/a2dl4THYWhthLBoMaQSRYqW5XbPo881WAJGi+lqRBjyOFryW6zpQluEkBy70zh+h9w==
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-equal "^1.0.1"
|
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.5.4"
|
prop-types "^15.5.4"
|
||||||
|
react-fast-compare "^2.0.2"
|
||||||
react-side-effect "^1.1.0"
|
react-side-effect "^1.1.0"
|
||||||
|
|
||||||
react-hot-loader@^4.3.8:
|
react-hot-loader@^4.3.8:
|
||||||
|
|
Loading…
Reference in New Issue