diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py index 7a348e06..3c1db9f5 100644 --- a/backend/grant/proposal/models.py +++ b/backend/grant/proposal/models.py @@ -2,6 +2,7 @@ import datetime from functools import reduce from sqlalchemy import func, or_ from sqlalchemy.ext.hybrid import hybrid_property +from decimal import Decimal from grant.comment.models import Comment from grant.email.send import send_email @@ -263,7 +264,7 @@ class Proposal(db.Model): self.deadline_duration = deadline_duration Proposal.validate(vars(self)) - def create_contribution(self, user_id: int, amount: float): + def create_contribution(self, user_id: int, amount): contribution = ProposalContribution( proposal_id=self.id, user_id=user_id, @@ -275,18 +276,16 @@ class Proposal(db.Model): def get_staking_contribution(self, user_id: int): contribution = None - remaining = PROPOSAL_STAKING_AMOUNT - float(self.contributed) + remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.contributed) # check funding if remaining > 0: - # find pending contribution for any user - # (always use full staking amout so we can find it) + # find pending contribution for any user of remaining amount 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) + contribution = self.create_contribution(user_id, str(remaining.normalize())) return contribution @@ -345,14 +344,14 @@ class Proposal(db.Model): contributions = ProposalContribution.query \ .filter_by(proposal_id=self.id, status=ContributionStatus.CONFIRMED) \ .all() - funded = reduce(lambda prev, c: prev + float(c.amount), contributions, 0) + funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0) return str(funded) @hybrid_property def funded(self): - target = float(self.target) + target = Decimal(self.target) # apply matching multiplier - funded = float(self.contributed) * (1 + self.contribution_matching) + funded = Decimal(self.contributed) * Decimal(1 + self.contribution_matching) # if funded > target, just set as target if funded > target: return str(target) @@ -361,7 +360,7 @@ class Proposal(db.Model): @hybrid_property def is_staked(self): - return float(self.contributed) >= PROPOSAL_STAKING_AMOUNT + return Decimal(self.contributed) >= PROPOSAL_STAKING_AMOUNT class ProposalSchema(ma.Schema): diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index a872aafb..16aeb148 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -237,6 +237,7 @@ def delete_proposal(proposal_id): ProposalStatus.PENDING, ProposalStatus.APPROVED, ProposalStatus.REJECTED, + ProposalStatus.STAKING, ] status = g.current_proposal.status if status not in deleteable_statuses: @@ -482,7 +483,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid): 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: + if contribution.proposal.is_staked: # Decimal(contribution.proposal.contributed) >= PROPOSAL_STAKING_AMOUNT: contribution.proposal.status = ProposalStatus.PENDING db.session.add(contribution.proposal) db.session.commit() @@ -493,7 +494,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid): 'proposal': contribution.proposal, 'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}', 'fully_staked': contribution.proposal.is_staked, - 'stake_target': PROPOSAL_STAKING_AMOUNT + 'stake_target': str(PROPOSAL_STAKING_AMOUNT.normalize()), }) else: diff --git a/backend/grant/settings.py b/backend/grant/settings.py index d91908f3..47a1c820 100644 --- a/backend/grant/settings.py +++ b/backend/grant/settings.py @@ -7,6 +7,7 @@ For local development, use a .env file to set environment variables. """ from environs import Env +from decimal import Decimal env = Env() env.read_env() @@ -54,7 +55,7 @@ ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH") EXPLORER_URL = env.str("EXPLORER_URL", default="https://explorer.zcha.in/") -PROPOSAL_STAKING_AMOUNT = env.float("PROPOSAL_STAKING_AMOUNT") +PROPOSAL_STAKING_AMOUNT = Decimal(env.str("PROPOSAL_STAKING_AMOUNT")) UI = { 'NAME': 'ZF Grants', diff --git a/backend/grant/templates/emails/staking_contribution_confirmed.html b/backend/grant/templates/emails/staking_contribution_confirmed.html index e742e005..a1b7fe93 100644 --- a/backend/grant/templates/emails/staking_contribution_confirmed.html +++ b/backend/grant/templates/emails/staking_contribution_confirmed.html @@ -5,9 +5,10 @@ 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. + {{ args.contribution.amount }} ZEC of the required + {{ args.stake_target}} ZEC. + You can send the remaining amount by going to your profile's "Pending" tab, + and clicking the "Stake" button next to the proposal. {% endif %} You can view your transaction below:
diff --git a/backend/grant/templates/emails/staking_contribution_confirmed.txt b/backend/grant/templates/emails/staking_contribution_confirmed.txt index 54afffd0..d562fc3d 100644 --- a/backend/grant/templates/emails/staking_contribution_confirmed.txt +++ b/backend/grant/templates/emails/staking_contribution_confirmed.txt @@ -3,9 +3,9 @@ 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. +{{ args.contribution.amount }} ZEC of the required {{ args.stake_target}} ZEC. +You can send the remaining amount by going to your profile's "Pending" tab, +and clicking the "Stake" button next to the proposal. {% endif %} You can view your transaction here: diff --git a/backend/tests/config.py b/backend/tests/config.py index d72f45c0..c9b6bcfc 100644 --- a/backend/tests/config.py +++ b/backend/tests/config.py @@ -155,7 +155,7 @@ class BaseProposalCreatorConfig(BaseUserConfig): # 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)) + contribution.confirm(tx_id='tx', amount=str(PROPOSAL_STAKING_AMOUNT.normalize())) db.session.add(contribution) db.session.commit() contribution = self.proposal.get_staking_contribution(self.user.id) diff --git a/backend/tests/proposal/test_api.py b/backend/tests/proposal/test_api.py index db2228c8..dff7a2c9 100644 --- a/backend/tests/proposal/test_api.py +++ b/backend/tests/proposal/test_api.py @@ -115,7 +115,7 @@ class TestProposalAPI(BaseProposalCreatorConfig): 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)) + self.assertEquals(resp.json['amount'], str(PROPOSAL_STAKING_AMOUNT.normalize())) @patch('requests.get', side_effect=mock_blockchain_api_requests) def test_proposal_stake_no_auth(self, mock_get): diff --git a/frontend/client/components/ContributionModal/index.tsx b/frontend/client/components/ContributionModal/index.tsx index 6155bb38..fe327a86 100644 --- a/frontend/client/components/ContributionModal/index.tsx +++ b/frontend/client/components/ContributionModal/index.tsx @@ -8,10 +8,12 @@ import PaymentInfo from './PaymentInfo'; interface OwnProps { isVisible: boolean; + contribution?: ContributionWithAddresses | Falsy; proposalId?: number; contributionId?: number; amount?: string; hasNoButtons?: boolean; + text?: React.ReactNode; handleClose(): void; } @@ -30,22 +32,32 @@ export default class ContributionModal extends React.Component