From fc86d6acbb07d34c277697a2287c04d99987bfa9 Mon Sep 17 00:00:00 2001 From: MiZiet <55240109+MiZiet@users.noreply.github.com> Date: Thu, 24 Dec 2020 11:20:02 +0100 Subject: [PATCH] Add validation for bounty proposal (#4265) * Add validation for bounty proposal * Add bounty title max length * Change title validation * Improve typing and naming in bounty validation * Change locales * Wrap validation into useEffect * Split validation Co-authored-by: Krzysztof Jelski --- .../apps/public/locales/en/app-bounties.json | 3 + packages/page-bounties/src/BountyCreate.tsx | 62 ++++++++++++++++--- .../src/helpers/calculateBountyBond.ts | 2 +- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/apps/public/locales/en/app-bounties.json b/packages/apps/public/locales/en/app-bounties.json index 469a740024..d8ab21f000 100644 --- a/packages/apps/public/locales/en/app-bounties.json +++ b/packages/apps/public/locales/en/app-bounties.json @@ -1,5 +1,7 @@ { + "Account does not have enough funds.": "Account does not have enough funds.", "Add Bounty": "Add Bounty", + "Allocation value is smaller than the minimum bounty value.": "Allocation value is smaller than the minimum bounty value.", "Bond": "Bond", "Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.": "Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.", "Bounties": "Bounties", @@ -18,6 +20,7 @@ "The total payment amount of this bounty, curators fee included.": "The total payment amount of this bounty, curators fee included.", "This account will propose the bounty. Bond amount will be reserved on its balance.": "This account will propose the bounty. Bond amount will be reserved on its balance.", "This amount will be reserved from origin account and returned on approval or slashed upon rejection.": "This amount will be reserved from origin account and returned on approval or slashed upon rejection.", + "Title too long": "Title too long", "Use this account to propose bounty from. This can be a normal or council account.": "Use this account to propose bounty from. This can be a normal or council account.", "Value": "Value", "beneficiary": "beneficiary", diff --git a/packages/page-bounties/src/BountyCreate.tsx b/packages/page-bounties/src/BountyCreate.tsx index 43c0520684..f524f209a8 100644 --- a/packages/page-bounties/src/BountyCreate.tsx +++ b/packages/page-bounties/src/BountyCreate.tsx @@ -4,15 +4,20 @@ import type { BalanceOf } from '@polkadot/types/interfaces'; import BN from 'bn.js'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { DeriveBalancesAll } from '@polkadot/api-derive/types'; import { Button, Input, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useToggle } from '@polkadot/react-hooks'; +import { useApi, useCall, useToggle } from '@polkadot/react-hooks'; import { BN_ZERO } from '@polkadot/util'; -import { calculateBountyBond } from './helpers'; +import { calculateBountyBond, countUtf8Bytes } from './helpers'; import { useTranslation } from './translate'; +const MIN_TITLE_LEN = 1; +const TITLE_DEFAULT_VALUE = ''; +const BOUNTY_DEFAULT_VALUE = BN_ZERO; + function BountyCreate () { const { t } = useTranslation(); const { api } = useApi(); @@ -20,9 +25,31 @@ function BountyCreate () { const [accountId, setAccountId] = useState(null); const [title, setTitle] = useState(''); const [bond, setBond] = useState(((api.consts.bounties || api.consts.treasury).bountyDepositBase as BalanceOf).toBn()); - const [amount, setAmount] = useState(BN_ZERO); - const [isValid] = useState(true); + const [value, setValue] = useState(BOUNTY_DEFAULT_VALUE); const [isOpen, toggleIsOpen] = useToggle(); + const [isTitleValid, setIsTitleValid] = useState(false); + const [isValueValid, setIsValueValid] = useState(false); + const [hasFunds, setHasFunds] = useState(false); + + const balances = useCall(api.derive.balances.all, [accountId]); + + useEffect(() => { + const bountyTitleMaxLength = ((api.consts.bounties || api.consts.treasury).maximumReasonLength as BalanceOf).toNumber(); + + setIsTitleValid(title?.length >= MIN_TITLE_LEN && countUtf8Bytes(title) <= bountyTitleMaxLength); + }, [api, title]); + + useEffect(() => { + const bountyMinValue = ((api.consts.bounties || api.consts.treasury).bountyValueMinimum as BalanceOf).toBn(); + + setIsValueValid(!!value?.gte(bountyMinValue)); + }, [api, value]); + + useEffect(() => { + setHasFunds(!!balances?.availableBalance.gte(bond)); + }, [balances, bond]); + + const isValid = hasFunds && isTitleValid && isValueValid; const onTitleChange = useCallback((value: string) => { const bountyBase = api.consts.bounties || api.consts.treasury; @@ -51,11 +78,18 @@ function BountyCreate () { ('The description of this bounty')} + isError={!isTitleValid} label={t('bounty title')} onChange={onTitleChange} value={title} /> + {!isTitleValid && (title !== TITLE_DEFAULT_VALUE) && ( +
+ {t('Title too long')} +
+ )}

{t('Description of the Bounty (to be stored on-chain)')}

@@ -65,11 +99,17 @@ function BountyCreate () { ('The total payment amount of this bounty, curators fee included.')} + isError={!isValueValid} isZeroable label={t('bounty requested allocation')} - onChange={setAmount} - value={amount} + onChange={setValue} + value={value} /> + {!isValueValid && !value?.eq(BOUNTY_DEFAULT_VALUE) && ( +
+ {t('Allocation value is smaller than the minimum bounty value.')} +
+ )}

{t('How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.')}

@@ -92,11 +132,17 @@ function BountyCreate () { ('Select the account you wish to propose the bounty from.')} + isError={!hasFunds} label={t('submit with account')} onChange={setAccountId} type='account' withLabel /> + {!hasFunds && ( +
+ {t('Account does not have enough funds.')} +
+ )}

{t('This account will propose the bounty. Bond amount will be reserved on its balance.')}

@@ -110,7 +156,7 @@ function BountyCreate () { isDisabled={!accountId || !isValid} label={t('Add Bounty')} onStart={toggleIsOpen} - params={[amount, title]} + params={[value, title]} tx={(api.tx.bounties || api.tx.treasury).proposeBounty} /> diff --git a/packages/page-bounties/src/helpers/calculateBountyBond.ts b/packages/page-bounties/src/helpers/calculateBountyBond.ts index bad2a896ed..6aceb41bfd 100644 --- a/packages/page-bounties/src/helpers/calculateBountyBond.ts +++ b/packages/page-bounties/src/helpers/calculateBountyBond.ts @@ -9,6 +9,6 @@ export function calculateBountyBond (description: string, depositBase: BalanceOf return depositBase.toBn().add(depositPerByte.toBn().muln(countUtf8Bytes(description))); } -function countUtf8Bytes (str: string) { +export function countUtf8Bytes (str: string): number { return new Blob([str]).size; }