diff --git a/packages/page-referenda/src/Referenda/Delegate/Activity.tsx b/packages/page-referenda/src/Referenda/Delegate/Activity.tsx new file mode 100644 index 0000000000..e7e0472d5b --- /dev/null +++ b/packages/page-referenda/src/Referenda/Delegate/Activity.tsx @@ -0,0 +1,79 @@ +// Copyright 2017-2023 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletReferenda, TrackDescription, TrackInfoExt } from '../../types'; +import type { VoteResultItem } from './types'; + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { MarkWarning, Table } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; + +import { useTranslation } from '../../translate'; +import { getTrackInfo } from '../../util'; + +interface Props { + allowEmpty?: boolean; + className?: string; + palletReferenda: PalletReferenda; + trackId: number; + tracks: TrackDescription[]; + value?: VoteResultItem[] | null | false; +} + +function Activity ({ allowEmpty, className, palletReferenda, tracks, value }: Props): React.ReactElement | null { + const { t } = useTranslation(); + const { api, specName } = useApi(); + + const infos = useMemo( + () => value && value.map((v): [VoteResultItem, TrackInfoExt | undefined] => + [v, getTrackInfo(api, specName, palletReferenda, tracks, v.classId.toNumber())] + ), + [api, palletReferenda, specName, tracks, value] + ); + + if (!infos) { + return null; + } + + return ( + + {infos.length + ? ( + + {infos.map(([{ casting, classId, delegating }, info], index) => ( + + + + + ))} +
+ {(info && info.trackName) || classId.toString()} + + { + (delegating && + t('delegating')) || + (casting && + `${casting.length} ${casting.length === 1 ? t('vote') : t('votes')}`) + } +
+ ) + : ('This account has no voting/delating activity in the chain state')} /> + } + {!allowEmpty && infos.some(([{ delegating }]) => delegating) && ( + ('This account has some delegations in itself')} /> + )} +
+ ); +} + +const StyledDiv = styled.div` + .ui--Table { + font-size: var(--font-percent-small); + opacity: var(--opacity-light); + padding-left: 2rem; + } +`; + +export default React.memo(Activity); diff --git a/packages/page-referenda/src/Referenda/Delegate/index.tsx b/packages/page-referenda/src/Referenda/Delegate/index.tsx new file mode 100644 index 0000000000..cea1d0f16e --- /dev/null +++ b/packages/page-referenda/src/Referenda/Delegate/index.tsx @@ -0,0 +1,285 @@ +// Copyright 2017-2023 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { PalletReferenda, PalletVote, TrackDescription } from '../../types'; + +import React, { useCallback, useMemo, useState } from 'react'; + +import { Button, ConvictionDropdown, InputAddress, Modal, Toggle, ToggleGroup, TxButton, VoteValue } from '@polkadot/react-components'; +import { useAccounts, useApi, useStepper, useToggle } from '@polkadot/react-hooks'; +import { isFunction } from '@polkadot/util'; + +import { useTranslation } from '../../translate'; +import TrackDropdown from '../Submit/TrackDropdown'; +import Activity from './Activity'; +import useActivityAccount from './useActivityAccount'; +import useActivityFellows from './useActivityFellows'; +import useActivityNominators from './useActivityNominators'; + +interface Props { + className?: string; + palletReferenda: PalletReferenda; + palletVote: PalletVote; + tracks: TrackDescription[]; +} + +interface Option { + key: string; + name: string; + value: string; +} + +function Delegate ({ className, palletReferenda, palletVote, tracks }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const { hasAccounts } = useAccounts(); + const [isOpen, toggleOpen] = useToggle(); + const [isAllTracks, toggleAllTracks] = useToggle(); + const [step, nextStep, prevStep] = useStepper(); + const [accountId, setAccountId] = useState(null); + const [toAccount, setToAccount] = useState(null); + const [trackId, setTrackId] = useState(0); + const [balance, setBalance] = useState(); + const [conviction, setConviction] = useState(1); + const activityFell = useActivityFellows(palletVote); + const activityVals = useActivityNominators(palletVote); + const activityFrom = useActivityAccount(palletVote, accountId); + const activityTo = useActivityAccount(palletVote, toAccount); + const [accType, setAccType] = useState({ index: 0, type: 'address' }); + + const includeTracks = useMemo( + () => activityFrom && tracks + .filter((t) => !activityFrom.some((a) => t.id.eq(a.classId))) + .map(({ id }) => id), + [activityFrom, tracks] + ); + + const allFell = useMemo( + // We also filter the fellows by activity - since there are a number + // we just want to skip those that does not have any activity + () => activityFell && + Object + .entries(activityFell) + .map(([key, act]) => (act.length > 0) && ({ key, name: key, value: key })) + .filter((a): a is Option => !!a), + [activityFell] + ); + + const allVals = useMemo( + () => activityVals && + Object + .entries(activityVals) + .map(([key]) => ({ key, name: key, value: key })) + .filter((a): a is Option => !!a), + [activityVals] + ); + + const typeOpts = useMemo( + () => [ + { text: t('Addresses'), value: 'address' }, + isFunction(api.query.staking?.nominators) && + { isDisabled: !allVals || !allVals.length, text: t('Validators'), value: 'validators' }, + isFunction(api.query.fellowshipCollective?.members) && + { isDisabled: !allFell || !allFell.length, text: t('Fellows'), value: 'fellows' } + ], + [allFell, allVals, api, t] + ); + + const onChangeType = useCallback( + (index: number, type: string | number) => + setAccType({ index, type: type.toString() }), + [] + ); + + const extrinsic = useMemo( + () => balance && conviction && toAccount && includeTracks + ? isAllTracks + ? api.tx.utility.forceBatch(includeTracks.map((trackId) => + api.tx[palletVote as 'convictionVoting'].delegate(trackId, toAccount, conviction, balance) + )) + : api.tx[palletVote as 'convictionVoting'].delegate(trackId, toAccount, conviction, balance) + : null, + [api, balance, conviction, includeTracks, isAllTracks, palletVote, toAccount, trackId] + ); + + const isStep1Valid = !!(accountId && activityFrom && includeTracks && (includeTracks.length > 0)); + const isStep2Valid = !!(toAccount && activityTo); + + return ( + <> + {isOpen && ( + ('Delegate votes {{step}}/{{numSteps}}', { replace: { numSteps: 2, step } })} + onClose={toggleOpen} + size='large' + > + {(step === 1) && ( + + ('Delegate from this account to another. All votes made on the target would count as a delegated vote for this account.')}> + ('delegate from account')} + onChange={setAccountId} + type='account' + withLabel + /> + + + ('Either delegate your votes for a single track as selected or delegate for all available tracks.')} + > + ('apply delegation to all tracks')} + onChange={toggleAllTracks} + value={isAllTracks} + /> + {!isAllTracks && includeTracks && (includeTracks.length > 0) && ( + + )} + + +

{t('The balance associated with the vote will be locked as per the conviction specified and will not be available for transfer during this period.')}

+

{t('Conviction locks do overlap and are not additive, meaning that funds locked during a previous vote can be locked again.')}

+ + } + > + ('delegated vote value')} + onChange={setBalance} + /> + ('conviction')} + onChange={setConviction} + value={conviction} + voteLockingPeriod={api.consts[palletVote as 'convictionVoting'].voteLockingPeriod} + /> +
+
+ )} + {(step === 2) && ( + + {(typeOpts.length > 1) && ( + ('Select from a list of pre-propulated accounts (based on your account activity) or supply your own')} + > + + + )} + ('The account that you wish to delegate to')}> + {accType.type === 'address' + ? ( + ('delegate to address')} + onChange={setToAccount} + type='allPlus' + /> + ) + : accType.type === 'validators' + ? ( + ('delegate to validator')} + onChange={setToAccount} + options={allVals} + type='allPlus' + /> + ) + : accType.type === 'fellows' + ? ( + ('delegate to fellow')} + onChange={setToAccount} + options={allFell} + type='allPlus' + /> + ) + : null + } + + + + )} + + {(step !== 1) && ( +