Proposal staking (#134)
* BE: proposal/<id>/stake end-point basics * BE: proposal staking tests * add STAKING to ProposalStatusEnum * BE: incremental staking related changes * admin: staking status * FE: proposal staking first pass * ZCash -> Zcash spelling * staking contribution confirmed email * FE: staking related minor style changes * FE proposal staking env var * notify user of partially staked proposal contributions
This commit is contained in:
parent
4091deaf2f
commit
f8f3bd1707
|
@ -1,6 +1,6 @@
|
||||||
# Zcash Grant System
|
# Zcash Grant System
|
||||||
|
|
||||||
This is a collection of the various services and components that make up the ZCash Grant System.
|
This is a collection of the various services and components that make up the Zcash Grant System.
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export enum PROPOSAL_STATUS {
|
||||||
REJECTED = 'REJECTED',
|
REJECTED = 'REJECTED',
|
||||||
LIVE = 'LIVE',
|
LIVE = 'LIVE',
|
||||||
DELETED = 'DELETED',
|
DELETED = 'DELETED',
|
||||||
|
STAKING = 'STAKING',
|
||||||
}
|
}
|
||||||
export interface Proposal {
|
export interface Proposal {
|
||||||
proposalId: number;
|
proposalId: number;
|
||||||
|
|
|
@ -52,6 +52,13 @@ export const PROPOSAL_STATUSES: Array<StatusSoT<PROPOSAL_STATUS>> = [
|
||||||
hint:
|
hint:
|
||||||
'Admin has rejected this proposal. User may adjust it and resubmit for approval.',
|
'Admin has rejected this proposal. User may adjust it and resubmit for approval.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: PROPOSAL_STATUS.STAKING,
|
||||||
|
filterDisplay: 'Status: staking',
|
||||||
|
tagDisplay: 'Staking',
|
||||||
|
tagColor: '#722ed1',
|
||||||
|
hint: 'This proposal is awaiting a staking contribution.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RFP_STATUSES: Array<StatusSoT<RFP_STATUS>> = [
|
export const RFP_STATUSES: Array<StatusSoT<RFP_STATUS>> = [
|
||||||
|
|
|
@ -31,3 +31,6 @@ ADMIN_PASS_HASH=18f97883b93a975deb9e29257a341a447302040da59cdc2d10ff65a5e57cc197
|
||||||
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
||||||
# EXPLORER_URL="https://explorer.zcha.in/"
|
# EXPLORER_URL="https://explorer.zcha.in/"
|
||||||
EXPLORER_URL="https://testnet.zcha.in/"
|
EXPLORER_URL="https://testnet.zcha.in/"
|
||||||
|
|
||||||
|
# Amount for staking a proposal in ZEC
|
||||||
|
PROPOSAL_STAKING_AMOUNT=0.025
|
||||||
|
|
|
@ -106,6 +106,21 @@ def proposal_comment(email_args):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def staking_contribution_confirmed(email_args):
|
||||||
|
subject = 'Your proposal has been staked!' if \
|
||||||
|
email_args['fully_staked'] else \
|
||||||
|
'Partial staking contribution confirmed'
|
||||||
|
return {
|
||||||
|
'subject': subject,
|
||||||
|
'title': 'Staking contribution confirmed',
|
||||||
|
'preview': 'Your {} ZEC staking contribution to {} has been confirmed!'.format(
|
||||||
|
email_args['contribution'].amount,
|
||||||
|
email_args['proposal'].title
|
||||||
|
),
|
||||||
|
'subscription': EmailSubscription.MY_PROPOSAL_FUNDED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def contribution_confirmed(email_args):
|
def contribution_confirmed(email_args):
|
||||||
return {
|
return {
|
||||||
'subject': 'Your contribution has been confirmed!',
|
'subject': 'Your contribution has been confirmed!',
|
||||||
|
@ -150,6 +165,7 @@ get_info_lookup = {
|
||||||
'proposal_rejected': proposal_rejected,
|
'proposal_rejected': proposal_rejected,
|
||||||
'proposal_contribution': proposal_contribution,
|
'proposal_contribution': proposal_contribution,
|
||||||
'proposal_comment': proposal_comment,
|
'proposal_comment': proposal_comment,
|
||||||
|
'staking_contribution_confirmed': staking_contribution_confirmed,
|
||||||
'contribution_confirmed': contribution_confirmed,
|
'contribution_confirmed': contribution_confirmed,
|
||||||
'contribution_update': contribution_update,
|
'contribution_update': contribution_update,
|
||||||
'comment_reply': comment_reply,
|
'comment_reply': comment_reply,
|
||||||
|
|
|
@ -10,15 +10,17 @@ from grant.utils.exceptions import ValidationException
|
||||||
from grant.utils.misc import dt_to_unix, make_url
|
from grant.utils.misc import dt_to_unix, make_url
|
||||||
from grant.utils.requests import blockchain_get
|
from grant.utils.requests import blockchain_get
|
||||||
from grant.utils.enums import ProposalStatus, ProposalStage, Category, ContributionStatus
|
from grant.utils.enums import ProposalStatus, ProposalStage, Category, ContributionStatus
|
||||||
|
from grant.settings import PROPOSAL_STAKING_AMOUNT
|
||||||
|
|
||||||
# Proposal states
|
# Proposal states
|
||||||
DRAFT = 'DRAFT'
|
DRAFT = 'DRAFT'
|
||||||
PENDING = 'PENDING'
|
PENDING = 'PENDING'
|
||||||
|
STAKING = 'STAKING'
|
||||||
APPROVED = 'APPROVED'
|
APPROVED = 'APPROVED'
|
||||||
REJECTED = 'REJECTED'
|
REJECTED = 'REJECTED'
|
||||||
LIVE = 'LIVE'
|
LIVE = 'LIVE'
|
||||||
DELETED = 'DELETED'
|
DELETED = 'DELETED'
|
||||||
STATUSES = [DRAFT, PENDING, APPROVED, REJECTED, LIVE, DELETED]
|
STATUSES = [DRAFT, PENDING, STAKING, APPROVED, REJECTED, LIVE, DELETED]
|
||||||
|
|
||||||
# Funding stages
|
# Funding stages
|
||||||
FUNDING_REQUIRED = 'FUNDING_REQUIRED'
|
FUNDING_REQUIRED = 'FUNDING_REQUIRED'
|
||||||
|
@ -255,14 +257,44 @@ class Proposal(db.Model):
|
||||||
self.deadline_duration = deadline_duration
|
self.deadline_duration = deadline_duration
|
||||||
Proposal.validate(vars(self))
|
Proposal.validate(vars(self))
|
||||||
|
|
||||||
|
def create_contribution(self, user_id: int, amount: float):
|
||||||
|
contribution = ProposalContribution(
|
||||||
|
proposal_id=self.id,
|
||||||
|
user_id=user_id,
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
db.session.add(contribution)
|
||||||
|
db.session.commit()
|
||||||
|
return contribution
|
||||||
|
|
||||||
|
def get_staking_contribution(self, user_id: int):
|
||||||
|
contribution = None
|
||||||
|
remaining = PROPOSAL_STAKING_AMOUNT - float(self.contributed)
|
||||||
|
# check funding
|
||||||
|
if remaining > 0:
|
||||||
|
# find pending contribution for any user
|
||||||
|
# (always use full staking amout so we can find it)
|
||||||
|
contribution = ProposalContribution.query.filter_by(
|
||||||
|
proposal_id=self.id,
|
||||||
|
amount=str(PROPOSAL_STAKING_AMOUNT),
|
||||||
|
status=PENDING,
|
||||||
|
).first()
|
||||||
|
if not contribution:
|
||||||
|
contribution = self.create_contribution(user_id, PROPOSAL_STAKING_AMOUNT)
|
||||||
|
|
||||||
|
return contribution
|
||||||
|
|
||||||
def submit_for_approval(self):
|
def submit_for_approval(self):
|
||||||
self.validate_publishable()
|
self.validate_publishable()
|
||||||
allowed_statuses = [ProposalStatus.DRAFT, ProposalStatus.REJECTED]
|
allowed_statuses = [ProposalStatus.DRAFT, ProposalStatus.REJECTED]
|
||||||
# specific validation
|
# specific validation
|
||||||
if self.status not in allowed_statuses:
|
if self.status not in allowed_statuses:
|
||||||
raise ValidationException(f"Proposal status must be draft or rejected to submit for approval")
|
raise ValidationException(f"Proposal status must be draft or rejected to submit for approval")
|
||||||
|
# set to PENDING if staked, else STAKING
|
||||||
|
if self.is_staked:
|
||||||
self.status = ProposalStatus.PENDING
|
self.status = ProposalStatus.PENDING
|
||||||
|
else:
|
||||||
|
self.status = ProposalStatus.STAKING
|
||||||
|
|
||||||
def approve_pending(self, is_approve, reject_reason=None):
|
def approve_pending(self, is_approve, reject_reason=None):
|
||||||
self.validate_publishable()
|
self.validate_publishable()
|
||||||
|
@ -321,6 +353,10 @@ class Proposal(db.Model):
|
||||||
|
|
||||||
return str(funded)
|
return str(funded)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def is_staked(self):
|
||||||
|
return float(self.contributed) >= PROPOSAL_STAKING_AMOUNT
|
||||||
|
|
||||||
|
|
||||||
class ProposalSchema(ma.Schema):
|
class ProposalSchema(ma.Schema):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -338,6 +374,7 @@ class ProposalSchema(ma.Schema):
|
||||||
"proposal_id",
|
"proposal_id",
|
||||||
"target",
|
"target",
|
||||||
"contributed",
|
"contributed",
|
||||||
|
"is_staked",
|
||||||
"funded",
|
"funded",
|
||||||
"content",
|
"content",
|
||||||
"comments",
|
"comments",
|
||||||
|
@ -383,6 +420,7 @@ user_fields = [
|
||||||
"title",
|
"title",
|
||||||
"brief",
|
"brief",
|
||||||
"target",
|
"target",
|
||||||
|
"is_staked",
|
||||||
"funded",
|
"funded",
|
||||||
"contribution_matching",
|
"contribution_matching",
|
||||||
"date_created",
|
"date_created",
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask_yoloapi import endpoint, parameter
|
||||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||||
from grant.email.send import send_email
|
from grant.email.send import send_email
|
||||||
from grant.milestone.models import Milestone
|
from grant.milestone.models import Milestone
|
||||||
from grant.settings import EXPLORER_URL
|
from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT
|
||||||
from grant.user.models import User
|
from grant.user.models import User
|
||||||
from grant.utils.auth import requires_auth, requires_team_member_auth, get_authed_user, internal_webhook
|
from grant.utils.auth import requires_auth, requires_team_member_auth, get_authed_user, internal_webhook
|
||||||
from grant.utils.exceptions import ValidationException
|
from grant.utils.exceptions import ValidationException
|
||||||
|
@ -242,6 +242,18 @@ def submit_for_approval_proposal(proposal_id):
|
||||||
return proposal_schema.dump(g.current_proposal), 200
|
return proposal_schema.dump(g.current_proposal), 200
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/<proposal_id>/stake", methods=["GET"])
|
||||||
|
@requires_team_member_auth
|
||||||
|
@endpoint.api()
|
||||||
|
def get_proposal_stake(proposal_id):
|
||||||
|
if g.current_proposal.status != ProposalStatus.STAKING:
|
||||||
|
return None, 400
|
||||||
|
contribution = g.current_proposal.get_staking_contribution(g.current_user.id)
|
||||||
|
if contribution:
|
||||||
|
return proposal_contribution_schema.dump(contribution)
|
||||||
|
return None, 404
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/<proposal_id>/publish", methods=["PUT"])
|
@blueprint.route("/<proposal_id>/publish", methods=["PUT"])
|
||||||
@requires_team_member_auth
|
@requires_team_member_auth
|
||||||
@endpoint.api()
|
@endpoint.api()
|
||||||
|
@ -419,13 +431,7 @@ def post_proposal_contribution(proposal_id, amount):
|
||||||
|
|
||||||
if not contribution:
|
if not contribution:
|
||||||
code = 201
|
code = 201
|
||||||
contribution = ProposalContribution(
|
contribution = proposal.create_contribution(g.current_user.id, amount)
|
||||||
proposal_id=proposal_id,
|
|
||||||
user_id=g.current_user.id,
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
db.session.add(contribution)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
||||||
return dumped_contribution, code
|
return dumped_contribution, code
|
||||||
|
@ -459,6 +465,23 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
db.session.add(contribution)
|
db.session.add(contribution)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
if contribution.proposal.status == ProposalStatus.STAKING:
|
||||||
|
# fully staked, set status PENDING & notify user
|
||||||
|
if contribution.proposal.is_staked: # float(contribution.proposal.contributed) >= PROPOSAL_STAKING_AMOUNT:
|
||||||
|
contribution.proposal.status = ProposalStatus.PENDING
|
||||||
|
db.session.add(contribution.proposal)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# email progress of staking, partial or complete
|
||||||
|
send_email(contribution.user.email_address, 'staking_contribution_confirmed', {
|
||||||
|
'contribution': contribution,
|
||||||
|
'proposal': contribution.proposal,
|
||||||
|
'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}',
|
||||||
|
'fully_staked': contribution.proposal.is_staked,
|
||||||
|
'stake_target': PROPOSAL_STAKING_AMOUNT
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
# Send to the user
|
# Send to the user
|
||||||
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
||||||
'contribution': contribution,
|
'contribution': contribution,
|
||||||
|
|
|
@ -54,6 +54,8 @@ ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
||||||
|
|
||||||
EXPLORER_URL = env.str("EXPLORER_URL", default="https://explorer.zcha.in/")
|
EXPLORER_URL = env.str("EXPLORER_URL", default="https://explorer.zcha.in/")
|
||||||
|
|
||||||
|
PROPOSAL_STAKING_AMOUNT = env.float("PROPOSAL_STAKING_AMOUNT")
|
||||||
|
|
||||||
UI = {
|
UI = {
|
||||||
'NAME': 'ZF Grants',
|
'NAME': 'ZF Grants',
|
||||||
'PRIMARY': '#CF8A00',
|
'PRIMARY': '#CF8A00',
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
<p style="margin: 0 0 20px;">
|
<p style="margin: 0 0 20px;">
|
||||||
Your <strong>{{ args.contribution.amount }} ZEC</strong> contribution has
|
Your <strong>{{ args.contribution.amount }} ZEC</strong> contribution has been
|
||||||
been confirmed! <strong>{{ args.proposal.title}}</strong> has been updated
|
confirmed! <strong>{{ args.proposal.title }}</strong> has been updated to
|
||||||
to reflect your funding, and your account will now show your contribution.
|
reflect your funding, and your account will now show your contribution. You
|
||||||
You can view your transaction below:
|
can view your transaction below:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 20px;">
|
<p style="margin: 0 0 20px;">
|
||||||
<a
|
<a href="{{ args.tx_explorer_url }}" target="_blank" rel="nofollow noopener">
|
||||||
href="{{ args.tx_explorer_url }}"
|
|
||||||
target="_blank"
|
|
||||||
rel="nofollow noopener"
|
|
||||||
>
|
|
||||||
{{ args.tx_explorer_url }}
|
{{ args.tx_explorer_url }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0;">
|
<p style="margin: 0;">
|
||||||
Thank you for your help in improving the ZCash ecosystem.
|
Thank you for your help in improving the Zcash ecosystem.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -4,4 +4,4 @@ account will now show your contribution. You can view your transaction here:
|
||||||
|
|
||||||
{{ args.tx_explorer_url }}
|
{{ args.tx_explorer_url }}
|
||||||
|
|
||||||
Thank you for your help in improving the ZCash ecosystem.
|
Thank you for your help in improving the Zcash ecosystem.
|
|
@ -0,0 +1,24 @@
|
||||||
|
<p style="margin: 0 0 20px;">
|
||||||
|
{% if args.fully_staked %}
|
||||||
|
<strong>{{ args.proposal.title }}</strong> has been
|
||||||
|
staked for <strong>{{ args.contribution.amount }} ZEC</strong>! Your proposal
|
||||||
|
will now be forwarded to administrators for approval.
|
||||||
|
{% else %}
|
||||||
|
<strong>{{ args.proposal.title }}</strong> has been partially staked for
|
||||||
|
<strong>{{ args.contribution.amount }} ZEC</strong>. This is not enough to
|
||||||
|
fully stake the proposal. You must send at least
|
||||||
|
<strong>{{ args.stake_target }} ZEC</strong>.
|
||||||
|
{% endif %}
|
||||||
|
You can view your transaction below:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 20px;">
|
||||||
|
<a href="{{ args.tx_explorer_url }}" target="_blank" rel="nofollow noopener">
|
||||||
|
{{ args.tx_explorer_url }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Thank you for your help in improving the Zcash ecosystem.
|
||||||
|
</p>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% if args.fully_staked %}
|
||||||
|
{{args.proposal.title}} has been staked for {{args.contribution.amount}} ZEC!
|
||||||
|
Your proposal will now be forwarded to administrators for approval.
|
||||||
|
{% else %}
|
||||||
|
{{ args.proposal.title }} has been partially staked for
|
||||||
|
{{ args.contribution.amount }} ZEC. This is not enough to
|
||||||
|
fully stake the proposal. You must send at least
|
||||||
|
{{ args.stake_target }} ZEC.
|
||||||
|
{% endif %}
|
||||||
|
You can view your transaction here:
|
||||||
|
|
||||||
|
{{ args.tx_explorer_url }}
|
||||||
|
|
||||||
|
Thank you for your help in improving the Zcash ecosystem.
|
|
@ -88,6 +88,7 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending):
|
||||||
result["comments"] = comments_dump
|
result["comments"] = comments_dump
|
||||||
if with_pending and authed_user and authed_user.id == user.id:
|
if with_pending and authed_user and authed_user.id == user.id:
|
||||||
pending = Proposal.get_by_user(user, [
|
pending = Proposal.get_by_user(user, [
|
||||||
|
ProposalStatus.STAKING,
|
||||||
ProposalStatus.PENDING,
|
ProposalStatus.PENDING,
|
||||||
ProposalStatus.APPROVED,
|
ProposalStatus.APPROVED,
|
||||||
ProposalStatus.REJECTED,
|
ProposalStatus.REJECTED,
|
||||||
|
|
|
@ -8,16 +8,21 @@ class CustomEnum():
|
||||||
class ProposalStatusEnum(CustomEnum):
|
class ProposalStatusEnum(CustomEnum):
|
||||||
DRAFT = 'DRAFT'
|
DRAFT = 'DRAFT'
|
||||||
PENDING = 'PENDING'
|
PENDING = 'PENDING'
|
||||||
|
STAKING = 'STAKING'
|
||||||
APPROVED = 'APPROVED'
|
APPROVED = 'APPROVED'
|
||||||
REJECTED = 'REJECTED'
|
REJECTED = 'REJECTED'
|
||||||
LIVE = 'LIVE'
|
LIVE = 'LIVE'
|
||||||
DELETED = 'DELETED'
|
DELETED = 'DELETED'
|
||||||
|
|
||||||
|
|
||||||
ProposalStatus = ProposalStatusEnum()
|
ProposalStatus = ProposalStatusEnum()
|
||||||
|
|
||||||
|
|
||||||
class ProposalStageEnum(CustomEnum):
|
class ProposalStageEnum(CustomEnum):
|
||||||
FUNDING_REQUIRED = 'FUNDING_REQUIRED'
|
FUNDING_REQUIRED = 'FUNDING_REQUIRED'
|
||||||
COMPLETED = 'COMPLETED'
|
COMPLETED = 'COMPLETED'
|
||||||
|
|
||||||
|
|
||||||
ProposalStage = ProposalStageEnum()
|
ProposalStage = ProposalStageEnum()
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +33,8 @@ class CategoryEnum(CustomEnum):
|
||||||
COMMUNITY = 'COMMUNITY'
|
COMMUNITY = 'COMMUNITY'
|
||||||
DOCUMENTATION = 'DOCUMENTATION'
|
DOCUMENTATION = 'DOCUMENTATION'
|
||||||
ACCESSIBILITY = 'ACCESSIBILITY'
|
ACCESSIBILITY = 'ACCESSIBILITY'
|
||||||
|
|
||||||
|
|
||||||
Category = CategoryEnum()
|
Category = CategoryEnum()
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +42,8 @@ class ContributionStatusEnum(CustomEnum):
|
||||||
PENDING = 'PENDING'
|
PENDING = 'PENDING'
|
||||||
CONFIRMED = 'CONFIRMED'
|
CONFIRMED = 'CONFIRMED'
|
||||||
DELETED = 'DELETED'
|
DELETED = 'DELETED'
|
||||||
|
|
||||||
|
|
||||||
ContributionStatus = ContributionStatusEnum()
|
ContributionStatus = ContributionStatusEnum()
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,4 +51,6 @@ class RFPStatusEnum(CustomEnum):
|
||||||
DRAFT = 'DRAFT'
|
DRAFT = 'DRAFT'
|
||||||
LIVE = 'LIVE'
|
LIVE = 'LIVE'
|
||||||
CLOSED = 'CLOSED'
|
CLOSED = 'CLOSED'
|
||||||
|
|
||||||
|
|
||||||
RFPStatus = RFPStatusEnum()
|
RFPStatus = RFPStatusEnum()
|
||||||
|
|
|
@ -3,6 +3,8 @@ from grant.utils.admin import generate_admin_password_hash
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
|
||||||
from ..config import BaseProposalCreatorConfig
|
from ..config import BaseProposalCreatorConfig
|
||||||
|
from ..mocks import mock_request
|
||||||
|
|
||||||
|
|
||||||
plaintext_mock_password = "p4ssw0rd"
|
plaintext_mock_password = "p4ssw0rd"
|
||||||
mock_admin_auth = {
|
mock_admin_auth = {
|
||||||
|
@ -96,8 +98,10 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
|
|
||||||
def test_approve_proposal(self):
|
def test_approve_proposal(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
# submit for approval (performed by end-user)
|
|
||||||
self.proposal.submit_for_approval()
|
# proposal needs to be PENDING
|
||||||
|
self.proposal.status = ProposalStatus.PENDING
|
||||||
|
|
||||||
# approve
|
# approve
|
||||||
resp = self.app.put(
|
resp = self.app.put(
|
||||||
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
||||||
|
@ -108,8 +112,10 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
|
|
||||||
def test_reject_proposal(self):
|
def test_reject_proposal(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
# submit for approval (performed by end-user)
|
|
||||||
self.proposal.submit_for_approval()
|
# proposal needs to be PENDING
|
||||||
|
self.proposal.status = ProposalStatus.PENDING
|
||||||
|
|
||||||
# reject
|
# reject
|
||||||
resp = self.app.put(
|
resp = self.app.put(
|
||||||
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import json
|
import json
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
from flask_testing import TestCase
|
from flask_testing import TestCase
|
||||||
from grant.app import create_app
|
from grant.app import create_app
|
||||||
from grant.proposal.models import Proposal
|
from grant.proposal.models import Proposal, ProposalContribution
|
||||||
from grant.task.jobs import ProposalReminder
|
from grant.task.jobs import ProposalReminder
|
||||||
from grant.user.models import User, SocialMedia, db, Avatar
|
from grant.user.models import User, SocialMedia, db, Avatar
|
||||||
|
from grant.settings import PROPOSAL_STAKING_AMOUNT
|
||||||
from grant.utils.enums import ProposalStatus
|
from grant.utils.enums import ProposalStatus
|
||||||
|
|
||||||
from .test_data import test_user, test_other_user, test_proposal
|
from .test_data import test_user, test_other_user, test_proposal, mock_contribution_addresses
|
||||||
|
|
||||||
|
|
||||||
class BaseTestConfig(TestCase):
|
class BaseTestConfig(TestCase):
|
||||||
|
@ -145,3 +147,16 @@ class BaseProposalCreatorConfig(BaseUserConfig):
|
||||||
def make_proposal_reminder_task(self):
|
def make_proposal_reminder_task(self):
|
||||||
proposal_reminder = ProposalReminder(self.proposal.id)
|
proposal_reminder = ProposalReminder(self.proposal.id)
|
||||||
proposal_reminder.make_task()
|
proposal_reminder.make_task()
|
||||||
|
|
||||||
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
|
def stake_proposal(self, mock_get):
|
||||||
|
# 1. submit
|
||||||
|
self.proposal.submit_for_approval()
|
||||||
|
# 2. get staking contribution
|
||||||
|
contribution = self.proposal.get_staking_contribution(self.user.id)
|
||||||
|
# 3. fake a confirmation
|
||||||
|
contribution.confirm(tx_id='tx', amount=str(PROPOSAL_STAKING_AMOUNT))
|
||||||
|
db.session.add(contribution)
|
||||||
|
db.session.commit()
|
||||||
|
contribution = self.proposal.get_staking_contribution(self.user.id)
|
||||||
|
return contribution
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import json
|
import json
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from grant.settings import PROPOSAL_STAKING_AMOUNT
|
||||||
from grant.proposal.models import Proposal
|
from grant.proposal.models import Proposal
|
||||||
from grant.utils.enums import ProposalStatus
|
from grant.utils.enums import ProposalStatus
|
||||||
|
|
||||||
from ..config import BaseProposalCreatorConfig
|
from ..config import BaseProposalCreatorConfig
|
||||||
from ..test_data import test_proposal
|
from ..test_data import test_proposal, mock_contribution_addresses
|
||||||
|
|
||||||
|
|
||||||
class TestProposalAPI(BaseProposalCreatorConfig):
|
class TestProposalAPI(BaseProposalCreatorConfig):
|
||||||
|
@ -70,6 +72,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
||||||
self.assert200(resp)
|
self.assert200(resp)
|
||||||
|
self.assertEqual(resp.json['status'], ProposalStatus.STAKING)
|
||||||
|
|
||||||
def test_no_auth_proposal_draft_submit_for_approval(self):
|
def test_no_auth_proposal_draft_submit_for_approval(self):
|
||||||
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
||||||
|
@ -86,12 +89,45 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
||||||
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/submit_for_approval".format(self.proposal.id))
|
||||||
self.assert400(resp)
|
self.assert400(resp)
|
||||||
|
|
||||||
|
# /stake
|
||||||
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
|
def test_proposal_stake(self, mock_get):
|
||||||
|
self.login_default_user()
|
||||||
|
self.proposal.status = ProposalStatus.STAKING
|
||||||
|
resp = self.app.get(f"/api/v1/proposals/{self.proposal.id}/stake")
|
||||||
|
print(resp)
|
||||||
|
self.assert200(resp)
|
||||||
|
self.assertEquals(resp.json['amount'], str(PROPOSAL_STAKING_AMOUNT))
|
||||||
|
|
||||||
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
|
def test_proposal_stake_no_auth(self, mock_get):
|
||||||
|
self.proposal.status = ProposalStatus.STAKING
|
||||||
|
resp = self.app.get(f"/api/v1/proposals/{self.proposal.id}/stake")
|
||||||
|
print(resp)
|
||||||
|
self.assert401(resp)
|
||||||
|
|
||||||
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
|
def test_proposal_stake_bad_status(self, mock_get):
|
||||||
|
self.login_default_user()
|
||||||
|
self.proposal.status = ProposalStatus.PENDING # should be staking
|
||||||
|
resp = self.app.get(f"/api/v1/proposals/{self.proposal.id}/stake")
|
||||||
|
print(resp)
|
||||||
|
self.assert400(resp)
|
||||||
|
|
||||||
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
|
def test_proposal_stake_funded(self, mock_get):
|
||||||
|
self.login_default_user()
|
||||||
|
# fake stake contribution with confirmation
|
||||||
|
self.stake_proposal()
|
||||||
|
resp = self.app.get(f"/api/v1/proposals/{self.proposal.id}/stake")
|
||||||
|
print(resp)
|
||||||
|
self.assert404(resp)
|
||||||
|
|
||||||
# /publish
|
# /publish
|
||||||
def test_publish_proposal_approved(self):
|
def test_publish_proposal_approved(self):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
# submit for approval, then approve
|
# proposal needs to be APPROVED
|
||||||
self.proposal.submit_for_approval()
|
self.proposal.status = ProposalStatus.APPROVED
|
||||||
self.proposal.approve_pending(True) # admin action
|
|
||||||
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
||||||
self.assert200(resp)
|
self.assert200(resp)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from mock import patch
|
||||||
|
|
||||||
from grant.proposal.models import Proposal
|
from grant.proposal.models import Proposal
|
||||||
from grant.utils.enums import ProposalStatus
|
from grant.utils.enums import ProposalStatus
|
||||||
from ..config import BaseUserConfig
|
from ..config import BaseProposalCreatorConfig
|
||||||
from ..test_data import test_proposal
|
from ..test_data import test_proposal
|
||||||
from ..mocks import mock_request
|
from ..mocks import mock_request
|
||||||
|
|
||||||
|
@ -14,24 +14,17 @@ mock_contribution_addresses = mock_request({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class TestProposalContributionAPI(BaseUserConfig):
|
class TestProposalContributionAPI(BaseProposalCreatorConfig):
|
||||||
@patch('requests.get', side_effect=mock_contribution_addresses)
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
def test_create_proposal_contribution(self, mock_blockchain_get):
|
def test_create_proposal_contribution(self, mock_blockchain_get):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
proposal_res = self.app.post(
|
|
||||||
"/api/v1/proposals/drafts",
|
|
||||||
data=json.dumps(test_proposal),
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
proposal_json = proposal_res.json
|
|
||||||
proposal_id = proposal_json["proposalId"]
|
|
||||||
|
|
||||||
contribution = {
|
contribution = {
|
||||||
"amount": "1.2345"
|
"amount": "1.2345"
|
||||||
}
|
}
|
||||||
|
|
||||||
post_res = self.app.post(
|
post_res = self.app.post(
|
||||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
"/api/v1/proposals/{}/contributions".format(self.proposal.id),
|
||||||
data=json.dumps(contribution),
|
data=json.dumps(contribution),
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
|
@ -41,20 +34,13 @@ class TestProposalContributionAPI(BaseUserConfig):
|
||||||
@patch('requests.get', side_effect=mock_contribution_addresses)
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
def test_create_duplicate_contribution(self, mock_blockchain_get):
|
def test_create_duplicate_contribution(self, mock_blockchain_get):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
proposal_res = self.app.post(
|
|
||||||
"/api/v1/proposals/drafts",
|
|
||||||
data=json.dumps(test_proposal),
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
proposal_json = proposal_res.json
|
|
||||||
proposal_id = proposal_json["proposalId"]
|
|
||||||
|
|
||||||
contribution = {
|
contribution = {
|
||||||
"amount": "1.2345"
|
"amount": "1.2345"
|
||||||
}
|
}
|
||||||
|
|
||||||
post_res = self.app.post(
|
post_res = self.app.post(
|
||||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
"/api/v1/proposals/{}/contributions".format(self.proposal.id),
|
||||||
data=json.dumps(contribution),
|
data=json.dumps(contribution),
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
|
@ -62,7 +48,7 @@ class TestProposalContributionAPI(BaseUserConfig):
|
||||||
self.assertStatus(post_res, 201)
|
self.assertStatus(post_res, 201)
|
||||||
|
|
||||||
dupe_res = self.app.post(
|
dupe_res = self.app.post(
|
||||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
"/api/v1/proposals/{}/contributions".format(self.proposal.id),
|
||||||
data=json.dumps(contribution),
|
data=json.dumps(contribution),
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
|
@ -72,27 +58,20 @@ class TestProposalContributionAPI(BaseUserConfig):
|
||||||
@patch('requests.get', side_effect=mock_contribution_addresses)
|
@patch('requests.get', side_effect=mock_contribution_addresses)
|
||||||
def test_get_proposal_contribution(self, mock_blockchain_get):
|
def test_get_proposal_contribution(self, mock_blockchain_get):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
proposal_res = self.app.post(
|
|
||||||
"/api/v1/proposals/drafts",
|
|
||||||
data=json.dumps(test_proposal),
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
proposal_json = proposal_res.json
|
|
||||||
proposal_id = proposal_json["proposalId"]
|
|
||||||
|
|
||||||
contribution = {
|
contribution = {
|
||||||
"amount": "1.2345"
|
"amount": "1.2345"
|
||||||
}
|
}
|
||||||
|
|
||||||
post_res = self.app.post(
|
post_res = self.app.post(
|
||||||
"/api/v1/proposals/{}/contributions".format(proposal_id),
|
"/api/v1/proposals/{}/contributions".format(self.proposal.id),
|
||||||
data=json.dumps(contribution),
|
data=json.dumps(contribution),
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
contribution_id = post_res.json['id']
|
contribution_id = post_res.json['id']
|
||||||
|
|
||||||
contribution_res = self.app.get(
|
contribution_res = self.app.get(
|
||||||
f'/api/v1/proposals/{proposal_id}/contributions/{contribution_id}'
|
f'/api/v1/proposals/{self.proposal.id}/contributions/{contribution_id}'
|
||||||
)
|
)
|
||||||
|
|
||||||
contribution = contribution_res.json
|
contribution = contribution_res.json
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from .mocks import mock_request
|
||||||
from grant.utils.enums import Category
|
from grant.utils.enums import Category
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,3 +58,9 @@ test_reply = {
|
||||||
"comment": "Test reply"
|
"comment": "Test reply"
|
||||||
# Fill in parentCommentId in test
|
# Fill in parentCommentId in test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mock_contribution_addresses = mock_request({
|
||||||
|
'transparent': 't123',
|
||||||
|
'sprout': 'z123',
|
||||||
|
'memo': '123',
|
||||||
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@ WEBHOOK_URL="http://localhost:5000/api/v1"
|
||||||
# REST Server Config
|
# REST Server Config
|
||||||
PORT="5051"
|
PORT="5051"
|
||||||
|
|
||||||
# ZCash Node (Defaults are for regtest)
|
# Zcash Node (Defaults are for regtest)
|
||||||
ZCASH_NODE_URL="http://localhost:18232"
|
ZCASH_NODE_URL="http://localhost:18232"
|
||||||
ZCASH_NODE_USERNAME="zcash_user"
|
ZCASH_NODE_USERNAME="zcash_user"
|
||||||
ZCASH_NODE_PASSWORD="zcash_password"
|
ZCASH_NODE_PASSWORD="zcash_password"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Blockchain Watcher
|
# Blockchain Watcher
|
||||||
|
|
||||||
Creates a websocket server that reads and reports on the activity of the ZCash
|
Creates a websocket server that reads and reports on the activity of the Zcash
|
||||||
blockchain. Communicates with a node over RPC.
|
blockchain. Communicates with a node over RPC.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
|
@ -15,3 +15,6 @@ BACKEND_URL=http://localhost:5000
|
||||||
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
||||||
# EXPLORER_URL="https://explorer.zcha.in/"
|
# EXPLORER_URL="https://explorer.zcha.in/"
|
||||||
EXPLORER_URL="https://testnet.zcha.in/"
|
EXPLORER_URL="https://testnet.zcha.in/"
|
||||||
|
|
||||||
|
# Amount for staking a proposal in ZEC
|
||||||
|
PROPOSAL_STAKING_AMOUNT=0.025
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# ZF Grants Frontend
|
# ZF Grants Frontend
|
||||||
|
|
||||||
This is the front-end component of ZCash Grant System.
|
This is the front-end component of Zcash Grant System.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,12 @@ import {
|
||||||
EmailSubscriptions,
|
EmailSubscriptions,
|
||||||
RFP,
|
RFP,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
import { formatUserForPost, formatProposalFromGet, formatUserFromGet, formatRFPFromGet } from 'utils/api';
|
import {
|
||||||
|
formatUserForPost,
|
||||||
|
formatProposalFromGet,
|
||||||
|
formatUserFromGet,
|
||||||
|
formatRFPFromGet,
|
||||||
|
} from 'utils/api';
|
||||||
|
|
||||||
export function getProposals(): Promise<{ data: Proposal[] }> {
|
export function getProposals(): Promise<{ data: Proposal[] }> {
|
||||||
return axios.get('/api/v1/proposals/').then(res => {
|
return axios.get('/api/v1/proposals/').then(res => {
|
||||||
|
@ -261,6 +266,12 @@ export function getProposalContribution(
|
||||||
return axios.get(`/api/v1/proposals/${proposalId}/contributions/${contributionId}`);
|
return axios.get(`/api/v1/proposals/${proposalId}/contributions/${contributionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProposalStakingContribution(
|
||||||
|
proposalId: number,
|
||||||
|
): Promise<{ data: ContributionWithAddresses }> {
|
||||||
|
return axios.get(`/api/v1/proposals/${proposalId}/stake`);
|
||||||
|
}
|
||||||
|
|
||||||
export function getRFPs(): Promise<{ data: RFP[] }> {
|
export function getRFPs(): Promise<{ data: RFP[] }> {
|
||||||
return axios.get('/api/v1/rfps/').then(res => {
|
return axios.get('/api/v1/rfps/').then(res => {
|
||||||
res.data = res.data.map(formatRFPFromGet);
|
res.data = res.data.map(formatRFPFromGet);
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
&-text {
|
&-text {
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-types {
|
&-types {
|
||||||
|
@ -23,8 +24,7 @@
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 1px 2px rgba(#000, 0.1),
|
box-shadow: 0 1px 2px rgba(#000, 0.1), 0 1px 4px rgba(#000, 0.2);
|
||||||
0 1px 4px rgba(#000, 0.2);
|
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Form, Input, Button, Icon, Radio, message } from 'antd';
|
import { Form, Input, Button, Icon, Radio, message } from 'antd';
|
||||||
import { RadioChangeEvent } from 'antd/lib/radio';
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||||
|
@ -11,6 +11,7 @@ import './PaymentInfo.less';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contribution?: ContributionWithAddresses | Falsy;
|
contribution?: ContributionWithAddresses | Falsy;
|
||||||
|
text?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendType = 'sprout' | 'transparent';
|
type SendType = 'sprout' | 'transparent';
|
||||||
|
@ -25,7 +26,7 @@ export default class PaymentInfo extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { contribution } = this.props;
|
const { contribution, text } = this.props;
|
||||||
const { sendType } = this.state;
|
const { sendType } = this.state;
|
||||||
let address;
|
let address;
|
||||||
let memo;
|
let memo;
|
||||||
|
@ -45,32 +46,27 @@ export default class PaymentInfo extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="PaymentInfo" layout="vertical">
|
<Form className="PaymentInfo" layout="vertical">
|
||||||
<p className="PaymentInfo-text">
|
<div className="PaymentInfo-text">
|
||||||
Thank you for contributing! Just send using whichever method works best for you,
|
{text || (
|
||||||
and we'll let you know when your contribution has been confirmed. Need help
|
<>
|
||||||
sending?
|
Thank you for contributing! Just send using whichever method works best for
|
||||||
{/* TODO: Help / FAQ page for sending */}
|
you, and we'll let you know when your contribution has been confirmed.
|
||||||
{' '}
|
</>
|
||||||
<a>Click here</a>.
|
)}
|
||||||
</p>
|
{/* TODO: Help / FAQ page for sending */} Need help sending? <a>Click here</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
className="PaymentInfo-types"
|
className="PaymentInfo-types"
|
||||||
onChange={this.handleChangeSendType}
|
onChange={this.handleChangeSendType}
|
||||||
value={sendType}
|
value={sendType}
|
||||||
>
|
>
|
||||||
<Radio.Button value="sprout">
|
<Radio.Button value="sprout">Z Address (Private)</Radio.Button>
|
||||||
Z Address (Private)
|
<Radio.Button value="transparent">T Address (Public)</Radio.Button>
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value="transparent">
|
|
||||||
T Address (Public)
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|
||||||
<div className="PaymentInfo-uri">
|
<div className="PaymentInfo-uri">
|
||||||
<div className={
|
<div className={classnames('PaymentInfo-uri-qr', !uri && 'is-loading')}>
|
||||||
classnames('PaymentInfo-uri-qr', !uri && 'is-loading')
|
|
||||||
}>
|
|
||||||
<span style={{ opacity: uri ? 1 : 0 }}>
|
<span style={{ opacity: uri ? 1 : 0 }}>
|
||||||
<QRCode value={uri || ''} />
|
<QRCode value={uri || ''} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -100,7 +96,7 @@ export default class PaymentInfo extends React.Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
<div className="PaymentInfo-fields-row">
|
<div className="PaymentInfo-fields-row">
|
||||||
<CopyInput
|
<CopyInput
|
||||||
label="ZCash CLI command"
|
label="Zcash CLI command"
|
||||||
help="Make sure you replace YOUR_ADDRESS with your actual address"
|
help="Make sure you replace YOUR_ADDRESS with your actual address"
|
||||||
value={cli}
|
value={cli}
|
||||||
/>
|
/>
|
||||||
|
@ -123,33 +119,29 @@ interface CopyInputProps {
|
||||||
isTextarea?: boolean;
|
isTextarea?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CopyInput: React.SFC<CopyInputProps> = ({ label, value, help, className, isTextarea }) => (
|
const CopyInput: React.SFC<CopyInputProps> = ({
|
||||||
<Form.Item
|
label,
|
||||||
className={classnames(
|
value,
|
||||||
'CopyInput',
|
help,
|
||||||
className,
|
className,
|
||||||
isTextarea && 'is-textarea',
|
isTextarea,
|
||||||
)}
|
}) => (
|
||||||
|
<Form.Item
|
||||||
|
className={classnames('CopyInput', className, isTextarea && 'is-textarea')}
|
||||||
label={label}
|
label={label}
|
||||||
help={help}
|
help={help}
|
||||||
>
|
>
|
||||||
{isTextarea ? (
|
{isTextarea ? (
|
||||||
<>
|
<>
|
||||||
<Input.TextArea value={value} readOnly rows={3} />
|
<Input.TextArea value={value} readOnly rows={3} />
|
||||||
<CopyToClipboard
|
<CopyToClipboard text={value || ''} onCopy={() => message.success('Copied!', 2)}>
|
||||||
text={value || ''}
|
|
||||||
onCopy={() => message.success('Copied!', 2)}
|
|
||||||
>
|
|
||||||
<Button icon="copy" />
|
<Button icon="copy" />
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Input value={value} readOnly />
|
<Input value={value} readOnly />
|
||||||
<CopyToClipboard
|
<CopyToClipboard text={value || ''} onCopy={() => message.success('Copied!', 2)}>
|
||||||
text={value || ''}
|
|
||||||
onCopy={() => message.success('Copied!', 2)}
|
|
||||||
>
|
|
||||||
<Button icon="copy" />
|
<Button icon="copy" />
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
@import '~styles/variables.less';
|
@import '~styles/variables.less';
|
||||||
|
|
||||||
.CreateFinal {
|
.CreateFinal {
|
||||||
position: absolute;
|
max-width: 550px;
|
||||||
top: 50%;
|
padding: 1rem;
|
||||||
left: 50%;
|
margin: 3rem auto;
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&-message {
|
&-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
.anticon {
|
.anticon {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
@ -28,4 +27,15 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-contribute {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-staked {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -5,7 +5,10 @@ import { Link } from 'react-router-dom';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { createActions } from 'modules/create';
|
import { createActions } from 'modules/create';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { getProposalStakingContribution } from 'api/api';
|
||||||
import './Final.less';
|
import './Final.less';
|
||||||
|
import PaymentInfo from 'components/ContributionModal/PaymentInfo';
|
||||||
|
import { ContributionWithAddresses } from 'types';
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
form: AppState['create']['form'];
|
form: AppState['create']['form'];
|
||||||
|
@ -19,13 +22,34 @@ interface DispatchProps {
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps;
|
type Props = StateProps & DispatchProps;
|
||||||
|
|
||||||
class CreateFinal extends React.Component<Props> {
|
const STATE = {
|
||||||
|
contribution: null as null | ContributionWithAddresses,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = typeof STATE;
|
||||||
|
|
||||||
|
class CreateFinal extends React.Component<Props, State> {
|
||||||
|
state = STATE;
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.submit();
|
this.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prev: Props) {
|
||||||
|
const { submittedProposal } = this.props;
|
||||||
|
if (!prev.submittedProposal && submittedProposal) {
|
||||||
|
if (!submittedProposal.isStaked) {
|
||||||
|
this.getStakingContribution();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { submittedProposal, submitError } = this.props;
|
const { submittedProposal, submitError } = this.props;
|
||||||
|
const { contribution } = this.state;
|
||||||
|
|
||||||
|
const ready = submittedProposal && (submittedProposal.isStaked || contribution);
|
||||||
|
const staked = submittedProposal && submittedProposal.isStaked;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (submitError) {
|
if (submitError) {
|
||||||
content = (
|
content = (
|
||||||
|
@ -40,24 +64,57 @@ class CreateFinal extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (submittedProposal) {
|
} else if (ready) {
|
||||||
content = (
|
content = (
|
||||||
|
<>
|
||||||
<div className="CreateFinal-message is-success">
|
<div className="CreateFinal-message is-success">
|
||||||
<Icon type="check-circle" />
|
<Icon type="check-circle" />
|
||||||
|
{staked && (
|
||||||
<div className="CreateFinal-message-text">
|
<div className="CreateFinal-message-text">
|
||||||
Your proposal has been submitted! Check your{' '}
|
Your proposal has been staked and submitted! Check your{' '}
|
||||||
<Link to={`/profile?tab=pending`}>profile's pending proposals tab</Link> to
|
<Link to={`/profile?tab=pending`}>profile's pending proposals tab</Link>{' '}
|
||||||
check its status.
|
to check its status.
|
||||||
</div>
|
</div>
|
||||||
{/* TODO - remove or rework depending on design choices */}
|
)}
|
||||||
{/* <div className="CreateFinal-message-text">
|
{!staked && (
|
||||||
Your proposal has been submitted!{' '}
|
<div className="CreateFinal-message-text">
|
||||||
<Link to={`/proposals/${submittedProposal.proposalUrlId}`}>
|
Your proposal has been submitted! Please send the staking contribution of{' '}
|
||||||
Click here
|
<b>{contribution && contribution.amount} ZEC</b> using the instructions
|
||||||
</Link>
|
below.
|
||||||
{' '}to check it out.
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!staked && (
|
||||||
|
<>
|
||||||
|
<div className="CreateFinal-contribute">
|
||||||
|
<PaymentInfo
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
If you cannot send the payment now, you may bring up these
|
||||||
|
instructions again by visiting your{' '}
|
||||||
|
<Link to={`/profile?tab=funded`}>profile's funded tab</Link>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Once your payment has been sent and confirmed, you will receive an
|
||||||
|
email. Visit your{' '}
|
||||||
|
<Link to={`/profile?tab=pending`}>
|
||||||
|
profile's pending proposals tab
|
||||||
|
</Link>{' '}
|
||||||
|
at any time to check its status.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contribution={contribution}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="CreateFinal-staked">
|
||||||
|
I'm finished, take me to{' '}
|
||||||
|
<Link to="/profile?tab=pending">my pending proposals</Link>!
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = <Loader size="large" tip="Submitting your proposal..." />;
|
content = <Loader size="large" tip="Submitting your proposal..." />;
|
||||||
|
@ -71,6 +128,14 @@ class CreateFinal extends React.Component<Props> {
|
||||||
this.props.submitProposal(this.props.form);
|
this.props.submitProposal(this.props.form);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getStakingContribution = async () => {
|
||||||
|
const { submittedProposal } = this.props;
|
||||||
|
if (submittedProposal) {
|
||||||
|
const res = await getProposalStakingContribution(submittedProposal.proposalId);
|
||||||
|
this.setState({ contribution: res.data });
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect<StateProps, DispatchProps, {}, AppState>(
|
export default connect<StateProps, DispatchProps, {}, AppState>(
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Modal, Alert } from 'antd';
|
|
||||||
import { getCreateWarnings } from 'modules/create/utils';
|
|
||||||
import { ProposalDraft } from 'types';
|
|
||||||
import './PublishWarningModal.less';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
proposal: ProposalDraft | null;
|
|
||||||
isVisible: boolean;
|
|
||||||
handleClose(): void;
|
|
||||||
handlePublish(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PublishWarningModal extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { proposal, isVisible, handleClose, handlePublish } = this.props;
|
|
||||||
const warnings = proposal ? getCreateWarnings(proposal) : [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={<>Confirm submit for approval</>}
|
|
||||||
visible={isVisible}
|
|
||||||
okText="Confirm submit"
|
|
||||||
cancelText="Never mind"
|
|
||||||
onOk={handlePublish}
|
|
||||||
onCancel={handleClose}
|
|
||||||
>
|
|
||||||
<div className="PublishWarningModal">
|
|
||||||
{!!warnings.length && (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
message="Some fields have warnings"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<ul>
|
|
||||||
{warnings.map(w => (
|
|
||||||
<li key={w}>{w}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<p>You can still submit, despite these warnings.</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p>
|
|
||||||
Are you sure you're ready to submit your proposal for approval? Once you’ve
|
|
||||||
done so, you won't be able to edit it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
.PublishWarningModal {
|
.SubmitWarningModal {
|
||||||
.ant-alert {
|
.ant-alert {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, Alert } from 'antd';
|
||||||
|
import { getCreateWarnings } from 'modules/create/utils';
|
||||||
|
import { ProposalDraft } from 'types';
|
||||||
|
import './SubmitWarningModal.less';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
proposal: ProposalDraft | null;
|
||||||
|
isVisible: boolean;
|
||||||
|
handleClose(): void;
|
||||||
|
handleSubmit(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SubmitWarningModal extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { proposal, isVisible, handleClose, handleSubmit } = this.props;
|
||||||
|
const warnings = proposal ? getCreateWarnings(proposal) : [];
|
||||||
|
|
||||||
|
const staked = proposal && proposal.isStaked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<>Confirm submission</>}
|
||||||
|
visible={isVisible}
|
||||||
|
okText={staked ? 'Submit' : `I'm ready to stake`}
|
||||||
|
cancelText="Never mind"
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={handleClose}
|
||||||
|
>
|
||||||
|
<div className="SubmitWarningModal">
|
||||||
|
{!!warnings.length && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="Some fields have warnings"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<ul>
|
||||||
|
{warnings.map(w => (
|
||||||
|
<li key={w}>{w}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p>You can still submit, despite these warnings.</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{staked && (
|
||||||
|
<p>
|
||||||
|
Are you sure you're ready to submit your proposal for approval? Once you’ve
|
||||||
|
done so, you won't be able to edit it.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!staked && (
|
||||||
|
<p>
|
||||||
|
Are you sure you're ready to submit your proposal? You will be asked to send
|
||||||
|
a staking contribution of <b>{process.env.PROPOSAL_STAKING_AMOUNT} ZEC</b>.
|
||||||
|
Once confirmed, the proposal will be submitted for approval by site
|
||||||
|
administrators.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import Payment from './Payment';
|
||||||
import Review from './Review';
|
import Review from './Review';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
import Final from './Final';
|
import Final from './Final';
|
||||||
import PublishWarningModal from './PublishWarningModal';
|
import SubmitWarningModal from './SubmitWarningModal';
|
||||||
import createExampleProposal from './example';
|
import createExampleProposal from './example';
|
||||||
import { createActions } from 'modules/create';
|
import { createActions } from 'modules/create';
|
||||||
import { ProposalDraft } from 'types';
|
import { ProposalDraft } from 'types';
|
||||||
|
@ -115,8 +115,8 @@ type Props = StateProps & DispatchProps & RouteComponentProps<any>;
|
||||||
interface State {
|
interface State {
|
||||||
step: CREATE_STEP;
|
step: CREATE_STEP;
|
||||||
isPreviewing: boolean;
|
isPreviewing: boolean;
|
||||||
isShowingPublishWarning: boolean;
|
isShowingSubmitWarning: boolean;
|
||||||
isPublishing: boolean;
|
isSubmitting: boolean;
|
||||||
isExample: boolean;
|
isExample: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,9 +134,9 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
step,
|
step,
|
||||||
isPreviewing: false,
|
isPreviewing: false,
|
||||||
isPublishing: false,
|
isSubmitting: false,
|
||||||
isExample: false,
|
isExample: false,
|
||||||
isShowingPublishWarning: false,
|
isShowingSubmitWarning: false,
|
||||||
};
|
};
|
||||||
this.debouncedUpdateForm = debounce(this.updateForm, 800);
|
this.debouncedUpdateForm = debounce(this.updateForm, 800);
|
||||||
this.historyUnlisten = this.props.history.listen(this.handlePop);
|
this.historyUnlisten = this.props.history.listen(this.handlePop);
|
||||||
|
@ -154,7 +154,7 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isSavingDraft } = this.props;
|
const { isSavingDraft } = this.props;
|
||||||
const { step, isPreviewing, isPublishing, isShowingPublishWarning } = this.state;
|
const { step, isPreviewing, isSubmitting, isShowingSubmitWarning } = this.state;
|
||||||
|
|
||||||
const info = STEP_INFO[step];
|
const info = STEP_INFO[step];
|
||||||
const currentIndex = STEP_ORDER.indexOf(step);
|
const currentIndex = STEP_ORDER.indexOf(step);
|
||||||
|
@ -163,7 +163,7 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
let showFooter = true;
|
let showFooter = true;
|
||||||
if (isPublishing) {
|
if (isSubmitting) {
|
||||||
content = <Final />;
|
content = <Final />;
|
||||||
showFooter = false;
|
showFooter = false;
|
||||||
} else if (isPreviewing) {
|
} else if (isPreviewing) {
|
||||||
|
@ -241,11 +241,11 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
{isSavingDraft && (
|
{isSavingDraft && (
|
||||||
<div className="CreateFlow-draftNotification">Saving draft...</div>
|
<div className="CreateFlow-draftNotification">Saving draft...</div>
|
||||||
)}
|
)}
|
||||||
<PublishWarningModal
|
<SubmitWarningModal
|
||||||
proposal={this.props.form}
|
proposal={this.props.form}
|
||||||
isVisible={isShowingPublishWarning}
|
isVisible={isShowingSubmitWarning}
|
||||||
handleClose={this.closePublishWarning}
|
handleClose={this.closePublishWarning}
|
||||||
handlePublish={this.startPublish}
|
handleSubmit={this.startSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -274,10 +274,10 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
this.setState({ isPreviewing: !this.state.isPreviewing });
|
this.setState({ isPreviewing: !this.state.isPreviewing });
|
||||||
};
|
};
|
||||||
|
|
||||||
private startPublish = () => {
|
private startSubmit = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isPublishing: true,
|
isSubmitting: true,
|
||||||
isShowingPublishWarning: false,
|
isShowingSubmitWarning: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -302,11 +302,11 @@ class CreateFlow extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private openPublishWarning = () => {
|
private openPublishWarning = () => {
|
||||||
this.setState({ isShowingPublishWarning: true });
|
this.setState({ isShowingSubmitWarning: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
private closePublishWarning = () => {
|
private closePublishWarning = () => {
|
||||||
this.setState({ isShowingPublishWarning: false });
|
this.setState({ isShowingSubmitWarning: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private fillInExample = () => {
|
private fillInExample = () => {
|
||||||
|
|
|
@ -55,6 +55,17 @@ class ProfilePending extends React.Component<Props, State> {
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
[STATUS.STAKING]: {
|
||||||
|
color: 'purple',
|
||||||
|
tag: 'Staking',
|
||||||
|
blurb: (
|
||||||
|
<div>
|
||||||
|
Awaiting staking contribution, you will recieve an email when staking has been
|
||||||
|
confirmed. If you staked this proposal you may check its status under the
|
||||||
|
"funded" tab.
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
[STATUS.PENDING]: {
|
[STATUS.PENDING]: {
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
tag: 'Pending',
|
tag: 'Pending',
|
||||||
|
|
|
@ -142,7 +142,8 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
blurb: (
|
blurb: (
|
||||||
<>
|
<>
|
||||||
Your proposal has been approved! It is currently only visible to the team.
|
Your proposal has been approved! It is currently only visible to the team.
|
||||||
Visit your <Link to="/profile?tab=pending">profile's pending tab</Link> to publish.
|
Visit your <Link to="/profile?tab=pending">profile's pending tab</Link> to
|
||||||
|
publish.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -151,11 +152,22 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
blurb: (
|
blurb: (
|
||||||
<>
|
<>
|
||||||
Your proposal was rejected and is only visible to the team. Visit your{' '}
|
Your proposal was rejected and is only visible to the team. Visit your{' '}
|
||||||
<Link to="/profile?tab=pending">profile's pending tab</Link> for more information.
|
<Link to="/profile?tab=pending">profile's pending tab</Link> for more
|
||||||
|
information.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
},
|
},
|
||||||
|
[STATUS.STAKING]: {
|
||||||
|
blurb: (
|
||||||
|
<>
|
||||||
|
Your proposal is awaiting a staking contribution. Visit your{' '}
|
||||||
|
<Link to="/profile?tab=pending">profile's pending tab</Link> for more
|
||||||
|
information.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
} as { [key in STATUS]: { blurb: ReactNode; type: AlertProps['type'] } };
|
} as { [key in STATUS]: { blurb: ReactNode; type: AlertProps['type'] } };
|
||||||
let banner = statusBanner[proposal.status];
|
let banner = statusBanner[proposal.status];
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
|
@ -196,11 +208,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
['is-expanded']: isBodyExpanded,
|
['is-expanded']: isBodyExpanded,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{proposal ? (
|
{proposal ? <Markdown source={proposal.content} /> : <Loader />}
|
||||||
<Markdown source={proposal.content} />
|
|
||||||
) : (
|
|
||||||
<Loader />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{showExpand && (
|
{showExpand && (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -174,6 +174,7 @@ export default function createReducer(
|
||||||
case types.SUBMIT_PROPOSAL_PENDING:
|
case types.SUBMIT_PROPOSAL_PENDING:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
submittedProposal: null,
|
||||||
isSubmitting: true,
|
isSubmitting: true,
|
||||||
submitError: null,
|
submitError: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -192,6 +192,7 @@ export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
|
||||||
stage: 'preview',
|
stage: 'preview',
|
||||||
category: draft.category || PROPOSAL_CATEGORY.DAPP,
|
category: draft.category || PROPOSAL_CATEGORY.DAPP,
|
||||||
team: draft.team,
|
team: draft.team,
|
||||||
|
isStaked: true,
|
||||||
milestones: draft.milestones.map((m, idx) => ({
|
milestones: draft.milestones.map((m, idx) => ({
|
||||||
index: idx,
|
index: idx,
|
||||||
title: m.title,
|
title: m.title,
|
||||||
|
|
|
@ -41,6 +41,8 @@ envProductionRequiredHandler(
|
||||||
'http://localhost:' + (process.env.PORT || 3000),
|
'http://localhost:' + (process.env.PORT || 3000),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
envProductionRequiredHandler('PROPOSAL_STAKING_AMOUNT', '0.025');
|
||||||
|
|
||||||
const appDirectory = fs.realpathSync(process.cwd());
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
.split(path.delimiter)
|
.split(path.delimiter)
|
||||||
|
@ -54,6 +56,7 @@ module.exports = () => {
|
||||||
EXPLORER_URL: process.env.EXPLORER_URL || 'https://chain.so/zcash/',
|
EXPLORER_URL: process.env.EXPLORER_URL || 'https://chain.so/zcash/',
|
||||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
PORT: process.env.PORT || 3000,
|
PORT: process.env.PORT || 3000,
|
||||||
|
PROPOSAL_STAKING_AMOUNT: process.env.PROPOSAL_STAKING_AMOUNT,
|
||||||
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
||||||
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
||||||
SENTRY_RELEASE: process.env.SENTRY_RELEASE || undefined,
|
SENTRY_RELEASE: process.env.SENTRY_RELEASE || undefined,
|
||||||
|
|
|
@ -159,6 +159,7 @@ export function generateProposal({
|
||||||
content: 'body',
|
content: 'body',
|
||||||
stage: 'FUNDING_REQUIRED',
|
stage: 'FUNDING_REQUIRED',
|
||||||
category: PROPOSAL_CATEGORY.COMMUNITY,
|
category: PROPOSAL_CATEGORY.COMMUNITY,
|
||||||
|
isStaked: true,
|
||||||
team: [
|
team: [
|
||||||
{
|
{
|
||||||
userid: 123,
|
userid: 123,
|
||||||
|
|
|
@ -34,6 +34,7 @@ export interface ProposalDraft {
|
||||||
team: User[];
|
team: User[];
|
||||||
invites: TeamInvite[];
|
invites: TeamInvite[];
|
||||||
status: STATUS;
|
status: STATUS;
|
||||||
|
isStaked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
||||||
|
@ -85,6 +86,7 @@ export interface UserProposal {
|
||||||
// NOTE: sync with backend/grant/proposal/models.py STATUSES
|
// NOTE: sync with backend/grant/proposal/models.py STATUSES
|
||||||
export enum STATUS {
|
export enum STATUS {
|
||||||
DRAFT = 'DRAFT',
|
DRAFT = 'DRAFT',
|
||||||
|
STAKING = 'STAKING',
|
||||||
PENDING = 'PENDING',
|
PENDING = 'PENDING',
|
||||||
APPROVED = 'APPROVED',
|
APPROVED = 'APPROVED',
|
||||||
REJECTED = 'REJECTED',
|
REJECTED = 'REJECTED',
|
||||||
|
|
Loading…
Reference in New Issue