diff --git a/packages/apps-config/src/api/params/proposalThresholds.ts b/packages/apps-config/src/api/params/proposalThresholds.ts index a6fdb72e6cd8..befb17f6d1d7 100644 --- a/packages/apps-config/src/api/params/proposalThresholds.ts +++ b/packages/apps-config/src/api/params/proposalThresholds.ts @@ -1,38 +1,36 @@ // Copyright 2017-2021 @polkadot/app-config authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ApiPromise } from '@polkadot/api'; - import { KULUPU_GENESIS, KUSAMA_GENESIS, POLKADOT_GENESIS } from '../constants'; -const PROPOSE_THRESHOLDS: Record = { - [KULUPU_GENESIS]: 1, - [KUSAMA_GENESIS]: 0.5, - [POLKADOT_GENESIS]: 0.6, - default: 0.5 -}; +export interface Threshold { + value: number; + option: 'AtLeast' | 'MoreThan'; +} -const SLASH_THRESHOLDS: Record = { - [KUSAMA_GENESIS]: 0.5, - [POLKADOT_GENESIS]: 0.75, - default: 0.5 +export const PROPOSE_THRESHOLDS: Record = { + [KULUPU_GENESIS]: { option: 'MoreThan', value: 0.8 }, + [KUSAMA_GENESIS]: { option: 'AtLeast', value: 0.5 }, + [POLKADOT_GENESIS]: { option: 'AtLeast', value: 0.6 }, + default: { option: 'AtLeast', value: 0.5 } }; -const TREASURY_THRESHOLDS: Record = { - [KULUPU_GENESIS]: 0.5, - [KUSAMA_GENESIS]: 0.6, - [POLKADOT_GENESIS]: 0.6, - default: 0.6 +export const REJECT_THRESHOLDS: Record = { + [KULUPU_GENESIS]: { option: 'MoreThan', value: 0.5 }, + [KUSAMA_GENESIS]: { option: 'MoreThan', value: 0.5 }, + [POLKADOT_GENESIS]: { option: 'MoreThan', value: 0.5 }, + default: { option: 'MoreThan', value: 0.5 } }; -export function getProposalThreshold (api: ApiPromise): number { - return PROPOSE_THRESHOLDS[api.genesisHash.toHex()] || PROPOSE_THRESHOLDS.default; -} - -export function getSlashProposalThreshold (api: ApiPromise): number { - return SLASH_THRESHOLDS[api.genesisHash.toHex()] || SLASH_THRESHOLDS.default; -} +export const SLASH_THRESHOLDS: Record = { + [KUSAMA_GENESIS]: { option: 'AtLeast', value: 0.5 }, + [POLKADOT_GENESIS]: { option: 'AtLeast', value: 0.75 }, + default: { option: 'AtLeast', value: 0.5 } +}; -export function getTreasuryProposalThreshold (api: ApiPromise): number { - return TREASURY_THRESHOLDS[api.genesisHash.toHex()] || TREASURY_THRESHOLDS.default; -} +export const TREASURY_THRESHOLDS: Record = { + [KULUPU_GENESIS]: { option: 'MoreThan', value: 0.5 }, + [KUSAMA_GENESIS]: { option: 'AtLeast', value: 0.6 }, + [POLKADOT_GENESIS]: { option: 'AtLeast', value: 0.6 }, + default: { option: 'AtLeast', value: 0.6 } +}; diff --git a/packages/page-bounties/src/BountyActions/BountyInitiateVoting.tsx b/packages/page-bounties/src/BountyActions/BountyInitiateVoting.tsx index a514d100dfaa..24c8b36ecb34 100644 --- a/packages/page-bounties/src/BountyActions/BountyInitiateVoting.tsx +++ b/packages/page-bounties/src/BountyActions/BountyInitiateVoting.tsx @@ -4,12 +4,11 @@ import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; import type { BountyIndex } from '@polkadot/types/interfaces'; -import BN from 'bn.js'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; -import { getTreasuryProposalThreshold } from '@polkadot/apps-config'; import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components'; import { useApi, useMembers, useToggle } from '@polkadot/react-hooks'; +import { useThresholds } from '@polkadot/react-hooks/useThresholds'; import { truncateTitle } from '../helpers'; import { useBounties } from '../hooks'; @@ -30,13 +29,7 @@ function BountyInitiateVoting ({ description, index, proposals }: Props): React. const { approveBounty, closeBounty } = useBounties(); const [isOpen, toggleOpen] = useToggle(); const [accountId, setAccountId] = useState(null); - const [threshold, setThreshold] = useState(); - - useEffect((): void => { - members && setThreshold( - new BN(Math.ceil(members.length * getTreasuryProposalThreshold(api))) - ); - }, [api, members]); + const { treasuryProposalThreshold } = useThresholds(); const approveBountyProposal = useRef(approveBounty(index)); const closeBountyProposal = useRef(closeBounty(index)); @@ -84,7 +77,7 @@ function BountyInitiateVoting ({ description, index, proposals }: Props): React. isDisabled={false} label={t('Approve')} onStart={toggleOpen} - params={[threshold, approveBountyProposal.current, approveBountyProposal.current.length]} + params={[treasuryProposalThreshold, approveBountyProposal.current, approveBountyProposal.current.length]} tx={api.tx.council.propose} /> ('Reject')} onStart={toggleOpen} - params={[threshold, closeBountyProposal.current, closeBountyProposal.current.length]} + params={[treasuryProposalThreshold, closeBountyProposal.current, closeBountyProposal.current.length]} tx={api.tx.council.propose} /> diff --git a/packages/page-bounties/src/BountyActions/ProposeCuratorAction.tsx b/packages/page-bounties/src/BountyActions/ProposeCuratorAction.tsx index 6f51fb13aa7e..69efdcdfb806 100644 --- a/packages/page-bounties/src/BountyActions/ProposeCuratorAction.tsx +++ b/packages/page-bounties/src/BountyActions/ProposeCuratorAction.tsx @@ -7,9 +7,8 @@ import type { Balance, BountyIndex } from '@polkadot/types/interfaces'; import BN from 'bn.js'; import React, { useEffect, useMemo, useState } from 'react'; -import { getTreasuryProposalThreshold } from '@polkadot/apps-config'; import { Button, InputAddress, InputBalance, MarkError, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useMembers, useToggle } from '@polkadot/react-hooks'; +import { useApi, useMembers, useThresholds, useToggle } from '@polkadot/react-hooks'; import { BN_ZERO } from '@polkadot/util'; import { truncateTitle } from '../helpers'; @@ -30,19 +29,14 @@ function ProposeCuratorAction ({ description, index, proposals, value }: Props): const { api } = useApi(); const { isMember, members } = useMembers(); const { proposeCurator } = useBounties(); + const { treasuryProposalThreshold } = useThresholds(); + const [isOpen, toggleOpen] = useToggle(); const [accountId, setAccountId] = useState(null); const [curatorId, setCuratorId] = useState(null); - const [threshold, setThreshold] = useState(); const [fee, setFee] = useState(BN_ZERO); const [isFeeValid, setIsFeeValid] = useState(false); - useEffect((): void => { - members && setThreshold( - new BN(Math.ceil(members.length * getTreasuryProposalThreshold(api))) - ); - }, [api, members]); - const proposeCuratorProposal = useMemo(() => curatorId && proposeCurator(index, curatorId, fee), [curatorId, fee, index, proposeCurator]); const isVotingInitiated = useMemo(() => proposals?.filter(({ proposal }) => BOUNTY_METHODS.includes(proposal.method)).length !== 0, [proposals]); @@ -124,7 +118,7 @@ function ProposeCuratorAction ({ description, index, proposals, value }: Props): isDisabled={!isFeeValid} label={t('Propose curator')} onStart={toggleOpen} - params={[threshold, proposeCuratorProposal, proposeCuratorProposal?.length]} + params={[treasuryProposalThreshold, proposeCuratorProposal, proposeCuratorProposal?.length]} tx={api.tx.council.propose} /> diff --git a/packages/page-bounties/src/BountyExtraActions/CloseBounty.tsx b/packages/page-bounties/src/BountyExtraActions/CloseBounty.tsx index 3391f86b70c5..2bb161a73db4 100644 --- a/packages/page-bounties/src/BountyExtraActions/CloseBounty.tsx +++ b/packages/page-bounties/src/BountyExtraActions/CloseBounty.tsx @@ -3,12 +3,10 @@ import type { BountyIndex } from '@polkadot/types/interfaces'; -import BN from 'bn.js'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; -import { getTreasuryProposalThreshold } from '@polkadot/apps-config'; import { InputAddress, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useMembers } from '@polkadot/react-hooks'; +import { useApi, useMembers, useThresholds } from '@polkadot/react-hooks'; import { truncateTitle } from '../helpers'; import { useBounties } from '../hooks'; @@ -25,14 +23,9 @@ function CloseBounty ({ description, index, toggleOpen }: Props): React.ReactEle const { api } = useApi(); const { members } = useMembers(); const { closeBounty } = useBounties(); - const [accountId, setAccountId] = useState(null); - const [threshold, setThreshold] = useState(); + const { treasuryRejectionThreshold } = useThresholds(); - useEffect((): void => { - members && setThreshold( - new BN(Math.ceil(members.length * getTreasuryProposalThreshold(api))) - ); - }, [api, members]); + const [accountId, setAccountId] = useState(null); const closeBountyProposal = useRef(closeBounty(index)); @@ -68,7 +61,7 @@ function CloseBounty ({ description, index, toggleOpen }: Props): React.ReactEle isDisabled={false} label={t('Close Bounty')} onStart={toggleOpen} - params={[threshold, closeBountyProposal.current, closeBountyProposal.current.length]} + params={[treasuryRejectionThreshold, closeBountyProposal.current, closeBountyProposal.current.length]} tx={api.tx.council.propose} /> diff --git a/packages/page-bounties/src/BountyExtraActions/SlashCurator.tsx b/packages/page-bounties/src/BountyExtraActions/SlashCurator.tsx index 1757895857d3..56df7cc5e200 100644 --- a/packages/page-bounties/src/BountyExtraActions/SlashCurator.tsx +++ b/packages/page-bounties/src/BountyExtraActions/SlashCurator.tsx @@ -3,13 +3,11 @@ import type { AccountId, BountyIndex } from '@polkadot/types/interfaces'; -import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { getTreasuryProposalThreshold } from '@polkadot/apps-config'; import { InputAddress, Modal, TxButton } from '@polkadot/react-components'; -import { useAccounts, useApi, useMembers } from '@polkadot/react-hooks'; +import { useAccounts, useApi, useMembers, useThresholds } from '@polkadot/react-hooks'; import { truncateTitle } from '../helpers'; import { useBounties } from '../hooks'; @@ -41,15 +39,9 @@ function SlashCurator ({ action, curatorId, description, index, toggleOpen }: Pr const { members } = useMembers(); const { unassignCurator } = useBounties(); const [accountId, setAccountId] = useState(null); - const [threshold, setThreshold] = useState(); + const { treasuryRejectionThreshold } = useThresholds(); const { allAccounts } = useAccounts(); - useEffect((): void => { - members && setThreshold( - new BN(Math.ceil(members.length * getTreasuryProposalThreshold(api))) - ); - }, [api, members]); - const unassignCuratorProposal = useMemo(() => unassignCurator(index), [index, unassignCurator]); const actionProperties = useMemo>(() => ({ @@ -67,7 +59,7 @@ function SlashCurator ({ action, curatorId, description, index, toggleOpen }: Pr filter: members, header: t('This action will create a Council motion to slash the Curator.'), helpMessage: t('The Curator that will be slashed.'), - params: [threshold, unassignCuratorProposal, unassignCuratorProposal?.length], + params: [treasuryRejectionThreshold, unassignCuratorProposal, unassignCuratorProposal?.length], proposingAccountTip: t('The council member that will create the motion, submission equates to an "aye" vote.'), tip: t("If the motion is approved, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state."), title: t('Slash curator'), @@ -77,13 +69,13 @@ function SlashCurator ({ action, curatorId, description, index, toggleOpen }: Pr filter: members, header: t('This action will create a Council motion to unassign the Curator.'), helpMessage: t('The Curator that will be unassigned'), - params: [threshold, unassignCuratorProposal, unassignCuratorProposal?.length], + params: [treasuryRejectionThreshold, unassignCuratorProposal, unassignCuratorProposal?.length], proposingAccountTip: t('The council member that will create the motion, submission equates to an "aye" vote.'), tip: t('If the motion is approved, the current Curator will be unassigned and the Bounty will return to the Funded state.'), title: t('Unassign curator'), tx: api.tx.council.propose } - }), [t, index, unassignCurator, api.tx.council.propose, allAccounts, members, threshold, unassignCuratorProposal]); + }), [t, index, unassignCurator, api.tx.council.propose, allAccounts, members, treasuryRejectionThreshold, unassignCuratorProposal]); const { filter, header, helpMessage, params, proposingAccountTip, tip, title, tx } = actionProperties[action]; diff --git a/packages/page-council/src/Motions/ProposeExternal.tsx b/packages/page-council/src/Motions/ProposeExternal.tsx index c280d6fbe79d..77203afc1933 100644 --- a/packages/page-council/src/Motions/ProposeExternal.tsx +++ b/packages/page-council/src/Motions/ProposeExternal.tsx @@ -5,9 +5,8 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import React, { useCallback, useEffect, useState } from 'react'; -import { getProposalThreshold } from '@polkadot/apps-config'; import { Button, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useToggle } from '@polkadot/react-hooks'; +import { useApi, useThresholds, useToggle } from '@polkadot/react-hooks'; import { isHex } from '@polkadot/util'; import { useTranslation } from '../translate'; @@ -31,13 +30,13 @@ interface ProposalState { function ProposeExternal ({ className = '', isMember, members }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); + const { proposalThreshold } = useThresholds(); + const [isVisible, toggleVisible] = useToggle(); const [accountId, setAcountId] = useState(null); const [{ proposal, proposalLength }, setProposal] = useState({ proposalLength: 0 }); const [{ hash, isHashValid }, setHash] = useState({ hash: '', isHashValid: false }); - const threshold = Math.ceil((members.length || 0) * getProposalThreshold(api)); - const _onChangeHash = useCallback( (hash?: string): void => setHash({ hash, isHashValid: isHex(hash, 256) }), [] @@ -108,13 +107,13 @@ function ProposeExternal ({ className = '', isMember, members }: Props): React.R ('Propose')} onStart={toggleVisible} params={ api.tx.council.propose.meta.args.length === 3 - ? [threshold, proposal, proposalLength] - : [threshold, proposal] + ? [proposalThreshold, proposal, proposalLength] + : [proposalThreshold, proposal] } tx={api.tx.council.propose} /> diff --git a/packages/page-council/src/Motions/ProposeMotion.tsx b/packages/page-council/src/Motions/ProposeMotion.tsx index bc2b3f33d0b6..ce2aad68fc41 100644 --- a/packages/page-council/src/Motions/ProposeMotion.tsx +++ b/packages/page-council/src/Motions/ProposeMotion.tsx @@ -6,9 +6,8 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import BN from 'bn.js'; import React, { useCallback, useEffect, useState } from 'react'; -import { getProposalThreshold } from '@polkadot/apps-config'; import { Button, Extrinsic, InputAddress, InputNumber, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useToggle } from '@polkadot/react-hooks'; +import { useApi, useThresholds, useToggle } from '@polkadot/react-hooks'; import { BN_ZERO } from '@polkadot/util'; import { useTranslation } from '../translate'; @@ -31,6 +30,7 @@ interface ProposalState { function Propose ({ isMember, members }: Props): React.ReactElement { const { t } = useTranslation(); const { api, apiDefaultTxSudo } = useApi(); + const { proposalThreshold } = useThresholds(); const [isOpen, toggleOpen] = useToggle(); const [accountId, setAcountId] = useState(null); const [{ proposal, proposalLength }, setProposal] = useState({ proposalLength: 0 }); @@ -39,10 +39,9 @@ function Propose ({ isMember, members }: Props): React.ReactElement { useEffect((): void => { members && setThreshold({ isThresholdValid: members.length !== 0, - threshold: new BN(Math.ceil(members.length * getProposalThreshold(api))) + threshold: new BN(proposalThreshold) }); - }, [api, members]); - + }, [api, members, proposalThreshold]); const _setMethod = useCallback( (proposal?: SubmittableExtrinsic<'promise'> | null) => setProposal({ proposal, diff --git a/packages/page-council/src/Motions/Slashing.tsx b/packages/page-council/src/Motions/Slashing.tsx index 6eb2211de3c5..9a3f1da8bba0 100644 --- a/packages/page-council/src/Motions/Slashing.tsx +++ b/packages/page-council/src/Motions/Slashing.tsx @@ -5,9 +5,8 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import React, { useEffect, useMemo, useState } from 'react'; -import { getSlashProposalThreshold } from '@polkadot/apps-config'; import { Button, Dropdown, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useAvailableSlashes, useToggle } from '@polkadot/react-hooks'; +import { useApi, useAvailableSlashes, useThresholds, useToggle } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; @@ -30,14 +29,13 @@ interface ProposalState { function Slashing ({ className = '', isMember, members }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); + const { slashProposalThreshold } = useThresholds(); const slashes = useAvailableSlashes(); const [isVisible, toggleVisible] = useToggle(); const [accountId, setAcountId] = useState(null); const [{ proposal, proposalLength }, setProposal] = useState({ proposal: null, proposalLength: 0 }); const [selectedEra, setSelectedEra] = useState(0); - const threshold = Math.ceil((members.length || 0) * getSlashProposalThreshold(api)); - const eras = useMemo( () => (slashes || []).map(([era, slashes]): Option => ({ text: t('era {{era}}, {{count}} slashes', { @@ -123,13 +121,13 @@ function Slashing ({ className = '', isMember, members }: Props): React.ReactEle ('Revert')} onStart={toggleVisible} params={ api.tx.council.propose.meta.args.length === 3 - ? [threshold, proposal, proposalLength] - : [threshold, proposal] + ? [slashProposalThreshold, proposal, proposalLength] + : [slashProposalThreshold, proposal] } tx={api.tx.council.propose} /> diff --git a/packages/page-staking/src/Slashes/index.tsx b/packages/page-staking/src/Slashes/index.tsx index 399c127430b5..2c816ded4917 100644 --- a/packages/page-staking/src/Slashes/index.tsx +++ b/packages/page-staking/src/Slashes/index.tsx @@ -8,9 +8,8 @@ import type { Slash, SlashEra } from './types'; import BN from 'bn.js'; import React, { useMemo, useState } from 'react'; -import { getSlashProposalThreshold } from '@polkadot/apps-config'; import { Table, ToggleGroup } from '@polkadot/react-components'; -import { useAccounts, useApi, useMembers } from '@polkadot/react-hooks'; +import { useAccounts, useMembers, useThresholds } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; import { useTranslation } from '../translate'; @@ -88,9 +87,10 @@ function calcSlashEras (slashes: [BN, UnappliedSlash[]][], ownStashes: StakerSta function Slashes ({ ownStashes = [], slashes }: Props): React.ReactElement | null { const { t } = useTranslation(); - const { api } = useApi(); const { allAccounts } = useAccounts(); const { members } = useMembers(); + const { slashProposalThreshold } = useThresholds(); + const [selectedIndex, setSelectedIndex] = useState(0); const rows = useMemo( @@ -120,8 +120,6 @@ function Slashes ({ ownStashes = [], slashes }: Props): React.ReactElement } councilId={councilId} - councilThreshold={councilThreshold} + councilThreshold={slashProposalThreshold} key={rows[selectedIndex].era.toString()} slash={rows[selectedIndex]} /> diff --git a/packages/page-treasury/src/Overview/Council.tsx b/packages/page-treasury/src/Overview/Council.tsx index b8898cb37bb3..ea1985b15ef2 100644 --- a/packages/page-treasury/src/Overview/Council.tsx +++ b/packages/page-treasury/src/Overview/Council.tsx @@ -6,9 +6,8 @@ import type { ProposalIndex } from '@polkadot/types/interfaces'; import React, { useEffect, useRef, useState } from 'react'; -import { getTreasuryProposalThreshold } from '@polkadot/apps-config'; import { Button, Dropdown, InputAddress, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useToggle } from '@polkadot/react-hooks'; +import { useApi, useThresholds, useToggle } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; @@ -26,12 +25,13 @@ interface ProposalState { function Council ({ id, isDisabled, members }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); + const { treasuryProposalThreshold, treasuryRejectionThreshold } = useThresholds(); + const [isOpen, toggleOpen] = useToggle(); const [accountId, setAccountId] = useState(null); const [councilType, setCouncilType] = useState('accept'); const [{ proposal, proposalLength }, setProposal] = useState({ proposalLength: 0 }); - - const threshold = Math.ceil((members?.length || 0) * getTreasuryProposalThreshold(api)); + const [threshold, setThreshold] = useState(0); const councilTypeOptRef = useRef([ { text: t('Acceptance proposal to council'), value: 'accept' }, @@ -43,8 +43,13 @@ function Council ({ id, isDisabled, members }: Props): React.ReactElement ? api.tx.treasury.rejectProposal(id) : api.tx.treasury.approveProposal(id); + const threshold = councilType === 'reject' + ? treasuryRejectionThreshold + : treasuryProposalThreshold; + setProposal({ proposal, proposalLength: proposal.length }); - }, [api, councilType, id]); + setThreshold(threshold); + }, [api, councilType, id, treasuryProposalThreshold, treasuryRejectionThreshold]); return ( <> diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 62c416974508..e3fed5104962 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -37,5 +37,6 @@ export { useSavedFlags } from './useSavedFlags'; export { useSudo } from './useSudo'; export { useToggle } from './useToggle'; export { useTreasury } from './useTreasury'; +export { useThresholds } from './useThresholds'; export { useVotingStatus } from './useVotingStatus'; export { useWeight } from './useWeight'; diff --git a/packages/react-hooks/src/useThreshold.spec.ts b/packages/react-hooks/src/useThreshold.spec.ts new file mode 100644 index 000000000000..ea5ff9125e01 --- /dev/null +++ b/packages/react-hooks/src/useThreshold.spec.ts @@ -0,0 +1,21 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { getAtLeastThresholdMembersCount, getMoreThanThresholdMembersCount } from './useThresholds'; + +describe('minimal members count', () => { + it('0 members', () => { + expect(getAtLeastThresholdMembersCount(0, 0.5)).toEqual(0); + expect(getMoreThanThresholdMembersCount(0, 0.5)).toEqual(0); + }); + + it('even number of members', () => { + expect(getAtLeastThresholdMembersCount(12, 0.5)).toEqual(6); + expect(getMoreThanThresholdMembersCount(12, 0.5)).toEqual(7); + }); + + it('odd number of members', () => { + expect(getAtLeastThresholdMembersCount(19, 0.5)).toEqual(10); + expect(getMoreThanThresholdMembersCount(19, 0.5)).toEqual(10); + }); +}); diff --git a/packages/react-hooks/src/useThresholds.ts b/packages/react-hooks/src/useThresholds.ts new file mode 100644 index 000000000000..dbce5bde9bdc --- /dev/null +++ b/packages/react-hooks/src/useThresholds.ts @@ -0,0 +1,67 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Threshold } from '@polkadot/apps-config'; + +import { useMemo } from 'react'; + +import { PROPOSE_THRESHOLDS, + REJECT_THRESHOLDS, + SLASH_THRESHOLDS, + TREASURY_THRESHOLDS } from '@polkadot/apps-config'; + +import { useApi } from './useApi'; +import { useMembers } from './useMembers'; + +export interface Thresholds { + proposalThreshold: number; + treasuryProposalThreshold: number; + treasuryRejectionThreshold: number; + slashProposalThreshold: number; +} + +export function getMoreThanThresholdMembersCount (membersCount: number, thresholdRatio: number): number { + if (membersCount === 0) { return 0; } + + return Math.floor(membersCount * thresholdRatio) + 1; +} + +export function getAtLeastThresholdMembersCount (membersCount: number, thresholdRatio: number): number { + return Math.ceil(membersCount * thresholdRatio); +} + +function getThreshold (membersCount: number, threshold: Threshold): number { + return threshold.option === 'AtLeast' + ? getAtLeastThresholdMembersCount(membersCount, threshold.value) + : getMoreThanThresholdMembersCount(membersCount, threshold.value); +} + +export function useThresholds () : Thresholds { + const { api } = useApi(); + const { members } = useMembers(); + + return useMemo((): Thresholds => { + const membersCount = members?.length; + + if (!membersCount) { + return { proposalThreshold: 0, slashProposalThreshold: 0, treasuryProposalThreshold: 0, treasuryRejectionThreshold: 0 }; + } + + const genesisHash = api.genesisHash.toHex(); + + return { + proposalThreshold: getThreshold(membersCount, + PROPOSE_THRESHOLDS[genesisHash] || PROPOSE_THRESHOLDS.default + ), + slashProposalThreshold: getThreshold(membersCount, + SLASH_THRESHOLDS[genesisHash] || SLASH_THRESHOLDS.default + ), + treasuryProposalThreshold: getThreshold(membersCount, + TREASURY_THRESHOLDS[genesisHash] || TREASURY_THRESHOLDS.default + ), + treasuryRejectionThreshold: getThreshold(membersCount, + REJECT_THRESHOLDS[genesisHash] || REJECT_THRESHOLDS.default + ) + }; + }, [api, members]); +}