From 6932d4339b40da1c420835fadac87399d576b053 Mon Sep 17 00:00:00 2001 From: Jaco Date: Sun, 31 Jul 2022 13:32:31 +0200 Subject: [PATCH] Basic referenda list (#7944) * Basic referenda list * preimage * Debug display * Adjust Referendum displays * shortHash calc * Show deposits * Remove debug column --- packages/apps-routing/src/index.ts | 2 + packages/apps-routing/src/referenda.ts | 23 ++++ .../src/{Hash.tsx => Preimage.tsx} | 33 ++--- .../src/{Hashes.tsx => Preimages.tsx} | 8 +- packages/page-preimages/src/index.tsx | 4 +- packages/page-preimages/src/types.ts | 12 ++ packages/page-preimages/src/useHashInfo.ts | 35 ------ packages/page-preimages/src/usePreimage.ts | 45 +++++++ .../src/{useHashes.ts => usePreimages.ts} | 4 +- packages/page-referenda/src/Deposit.tsx | 42 +++++++ packages/page-referenda/src/RefOngoing.tsx | 80 +++++++++++++ packages/page-referenda/src/RefOther.tsx | 19 +++ packages/page-referenda/src/RefTuple.tsx | 69 +++++++++++ packages/page-referenda/src/Referenda.tsx | 58 +++++++++ packages/page-referenda/src/Referendum.tsx | 41 +++++++ packages/page-referenda/src/Vote.tsx | 113 ++++++++++++++++++ packages/page-referenda/src/index.tsx | 30 ++++- packages/page-referenda/src/types.ts | 23 ++++ packages/page-referenda/src/useIds.ts | 41 +++++++ packages/page-referenda/src/useReferenda.ts | 78 ++++++++++++ .../react-components/src/ProposedAction.tsx | 8 +- 21 files changed, 692 insertions(+), 76 deletions(-) create mode 100644 packages/apps-routing/src/referenda.ts rename packages/page-preimages/src/{Hash.tsx => Preimage.tsx} (51%) rename packages/page-preimages/src/{Hashes.tsx => Preimages.tsx} (89%) create mode 100644 packages/page-preimages/src/types.ts delete mode 100644 packages/page-preimages/src/useHashInfo.ts create mode 100644 packages/page-preimages/src/usePreimage.ts rename packages/page-preimages/src/{useHashes.ts => usePreimages.ts} (91%) create mode 100644 packages/page-referenda/src/Deposit.tsx create mode 100644 packages/page-referenda/src/RefOngoing.tsx create mode 100644 packages/page-referenda/src/RefOther.tsx create mode 100644 packages/page-referenda/src/RefTuple.tsx create mode 100644 packages/page-referenda/src/Referenda.tsx create mode 100644 packages/page-referenda/src/Referendum.tsx create mode 100644 packages/page-referenda/src/Vote.tsx create mode 100644 packages/page-referenda/src/types.ts create mode 100644 packages/page-referenda/src/useIds.ts create mode 100644 packages/page-referenda/src/useReferenda.ts diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 1d3920516a..c2616e7897 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -25,6 +25,7 @@ import nfts from './nfts'; import parachains from './parachains'; import poll from './poll'; import preimages from './preimages'; +import referenda from './referenda'; import rpc from './rpc'; import runtime from './runtime'; import settings from './settings'; @@ -52,6 +53,7 @@ export default function create (t: TFunction): Routes { staking(t), collator(t), democracy(t), + referenda(t), council(t), treasury(t), bounties(t), diff --git a/packages/apps-routing/src/referenda.ts b/packages/apps-routing/src/referenda.ts new file mode 100644 index 0000000000..c2a22cea19 --- /dev/null +++ b/packages/apps-routing/src/referenda.ts @@ -0,0 +1,23 @@ +// Copyright 2017-2022 @polkadot/apps-routing authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { TFunction } from 'i18next'; +import type { Route } from './types'; + +import Component from '@polkadot/app-referenda'; + +export default function create (t: TFunction): Route { + return { + Component, + display: { + needsApi: [ + 'tx.referenda.submit', + 'tx.convictionVoting.vote' + ] + }, + group: 'governance', + icon: 'person-booth', + name: 'referenda', + text: t('nav.referenda', 'Referenda', { ns: 'apps-routing' }) + }; +} diff --git a/packages/page-preimages/src/Hash.tsx b/packages/page-preimages/src/Preimage.tsx similarity index 51% rename from packages/page-preimages/src/Hash.tsx rename to packages/page-preimages/src/Preimage.tsx index f9a2fdeb13..ce7c1b5cc3 100644 --- a/packages/page-preimages/src/Hash.tsx +++ b/packages/page-preimages/src/Preimage.tsx @@ -1,43 +1,24 @@ // Copyright 2017-2022 @polkadot/app-preimages authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ApiPromise } from '@polkadot/api'; -import type { Bytes } from '@polkadot/types'; -import type { Call } from '@polkadot/types/interfaces'; import type { HexString } from '@polkadot/util/types'; -import React, { useMemo } from 'react'; +import React from 'react'; import { CallExpander } from '@polkadot/react-components'; -import { useApi } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; import { useTranslation } from './translate'; -import useHashInfo from './useHashInfo'; +import usePreimage from './usePreimage'; interface Props { className?: string; value: HexString; } -function decodeBytes (api: ApiPromise, bytes: Bytes): Call | null { - try { - return api.registry.createType('Call', bytes.toU8a(true)); - } catch (error) { - console.error(error); - } - - return null; -} - -function Hash ({ className, value }: Props): React.ReactElement { +function Preimage ({ className, value }: Props): React.ReactElement { const { t } = useTranslation(); - const { api } = useApi(); - const info = useHashInfo(value); - const decoded = useMemo( - () => info && info.bytes && decodeBytes(api, info.bytes), - [api, info] - ); + const info = usePreimage(value); return ( @@ -45,10 +26,10 @@ function Hash ({ className, value }: Props): React.ReactElement { {value} - {decoded && ( + {info && info.proposal && ( ('proposal')} - value={decoded} + value={info.proposal} /> )} @@ -62,4 +43,4 @@ function Hash ({ className, value }: Props): React.ReactElement { ); } -export default React.memo(Hash); +export default React.memo(Preimage); diff --git a/packages/page-preimages/src/Hashes.tsx b/packages/page-preimages/src/Preimages.tsx similarity index 89% rename from packages/page-preimages/src/Hashes.tsx rename to packages/page-preimages/src/Preimages.tsx index 77727f7bf2..b00e59ee7d 100644 --- a/packages/page-preimages/src/Hashes.tsx +++ b/packages/page-preimages/src/Preimages.tsx @@ -8,9 +8,9 @@ import React, { useRef } from 'react'; import { Button, Table } from '@polkadot/react-components'; import Add from './Add'; -import Hash from './Hash'; +import Preimage from './Preimage'; import { useTranslation } from './translate'; -import useHashes from './useHashes'; +import usePreimages from './usePreimages'; interface Props { className?: string; @@ -20,7 +20,7 @@ interface Props { function Hashes ({ className }: Props): React.ReactElement { const { t } = useTranslation(); - const hashes = useHashes(); + const hashes = usePreimages(); const headerRef = useRef([ [t('preimages'), 'start', 2], @@ -39,7 +39,7 @@ function Hashes ({ className }: Props): React.ReactElement { header={headerRef.current} > {hashes && hashes.map((h) => ( - diff --git a/packages/page-preimages/src/index.tsx b/packages/page-preimages/src/index.tsx index 8d24412449..2fc99d196d 100644 --- a/packages/page-preimages/src/index.tsx +++ b/packages/page-preimages/src/index.tsx @@ -5,7 +5,7 @@ import React, { useRef } from 'react'; import { Tabs } from '@polkadot/react-components'; -import Hashes from './Hashes'; +import Preimages from './Preimages'; import { useTranslation } from './translate'; interface Props { @@ -30,7 +30,7 @@ function App ({ basePath, className }: Props): React.ReactElement { basePath={basePath} items={tabsRef.current} /> - + ); } diff --git a/packages/page-preimages/src/types.ts b/packages/page-preimages/src/types.ts new file mode 100644 index 0000000000..e9ac7ed753 --- /dev/null +++ b/packages/page-preimages/src/types.ts @@ -0,0 +1,12 @@ +// Copyright 2017-2022 @polkadot/app-preimages authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Bytes } from '@polkadot/types'; +import type { Call } from '@polkadot/types/interfaces'; +import type { PalletPreimageRequestStatus } from '@polkadot/types/lookup'; + +export interface Preimage { + bytes: Bytes | null; + proposal: Call | null; + status: PalletPreimageRequestStatus | null; +} diff --git a/packages/page-preimages/src/useHashInfo.ts b/packages/page-preimages/src/useHashInfo.ts deleted file mode 100644 index af74e0557d..0000000000 --- a/packages/page-preimages/src/useHashInfo.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2017-2022 @polkadot/app-preimages authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import type { Bytes, Option } from '@polkadot/types'; -import type { PalletPreimageRequestStatus } from '@polkadot/types/lookup'; -import type { HexString } from '@polkadot/util/types'; - -import { useMemo } from 'react'; - -import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; - -interface Result { - bytes: Bytes | null; - status: PalletPreimageRequestStatus | null; -} - -function createResult (preimage: Option, status: Option): Result { - return { - bytes: preimage.unwrapOr(null), - status: status.unwrapOr(null) - }; -} - -function useHashInfoImpl (hash: HexString): Result | undefined { - const { api } = useApi(); - const preimage = useCall>(api.query.preimage.preimageFor, [hash]); - const status = useCall>(api.query.preimage.statusFor, [hash]); - - return useMemo( - () => preimage && status && createResult(preimage, status), - [preimage, status] - ); -} - -export default createNamedHook('useHashInfo', useHashInfoImpl); diff --git a/packages/page-preimages/src/usePreimage.ts b/packages/page-preimages/src/usePreimage.ts new file mode 100644 index 0000000000..fd32d05260 --- /dev/null +++ b/packages/page-preimages/src/usePreimage.ts @@ -0,0 +1,45 @@ +// Copyright 2017-2022 @polkadot/app-preimages authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Bytes, Option } from '@polkadot/types'; +import type { Call, Hash } from '@polkadot/types/interfaces'; +import type { PalletPreimageRequestStatus } from '@polkadot/types/lookup'; +import type { HexString } from '@polkadot/util/types'; +import type { Preimage } from './types'; + +import { useMemo } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function createResult (api: ApiPromise, preimage: Option, status: Option): Preimage { + const bytes = preimage.unwrapOr(null); + let proposal: Call | null = null; + + if (bytes) { + try { + proposal = api.registry.createType('Call', bytes.toU8a(true)); + } catch (error) { + console.error(error); + } + } + + return { + bytes, + proposal, + status: status.unwrapOr(null) + }; +} + +function usePreimageImpl (hash: Hash | HexString): Preimage | undefined { + const { api } = useApi(); + const preimage = useCall>(api.query.preimage.preimageFor, [hash]); + const status = useCall>(api.query.preimage.statusFor, [hash]); + + return useMemo( + () => preimage && status && createResult(api, preimage, status), + [api, preimage, status] + ); +} + +export default createNamedHook('usePreimage', usePreimageImpl); diff --git a/packages/page-preimages/src/useHashes.ts b/packages/page-preimages/src/usePreimages.ts similarity index 91% rename from packages/page-preimages/src/useHashes.ts rename to packages/page-preimages/src/usePreimages.ts index f44029d9ec..4a6e4cb1c5 100644 --- a/packages/page-preimages/src/useHashes.ts +++ b/packages/page-preimages/src/usePreimages.ts @@ -30,7 +30,7 @@ function filter (records: EventRecord[]): Changes { return { added, removed }; } -function useHashesImpl (): HexString[] | undefined { +function usePreimagesImpl (): HexString[] | undefined { const { api } = useApi(); const startValue = useMapKeys(api.query.preimage.statusFor, OPT_HASH); const hashes = useEventChanges([ @@ -44,4 +44,4 @@ function useHashesImpl (): HexString[] | undefined { ); } -export default createNamedHook('useHashes', useHashesImpl); +export default createNamedHook('usePreimages', usePreimagesImpl); diff --git a/packages/page-referenda/src/Deposit.tsx b/packages/page-referenda/src/Deposit.tsx new file mode 100644 index 0000000000..990f804bca --- /dev/null +++ b/packages/page-referenda/src/Deposit.tsx @@ -0,0 +1,42 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option } from '@polkadot/types'; +import type { PalletReferendaDeposit } from '@polkadot/types/lookup'; + +import React, { useMemo } from 'react'; + +import { AddressMini } from '@polkadot/react-components'; + +interface Props { + decision: Option | null; + submit: PalletReferendaDeposit | null; +} + +function Tuple ({ decision, submit }: Props): React.ReactElement { + const [first, second] = useMemo( + () => [submit, decision && decision.unwrapOr(null)], + [decision, submit] + ); + + return ( + + {first && ( + + )} + {second && ( + + )} + + ); +} + +export default React.memo(Tuple); diff --git a/packages/page-referenda/src/RefOngoing.tsx b/packages/page-referenda/src/RefOngoing.tsx new file mode 100644 index 0000000000..1116c48257 --- /dev/null +++ b/packages/page-referenda/src/RefOngoing.tsx @@ -0,0 +1,80 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { Referendum, ReferendumProps as Props } from './types'; + +import React, { useMemo } from 'react'; + +import usePreimage from '@polkadot/app-preimages/usePreimage'; +import { CallExpander, Progress } from '@polkadot/react-components'; + +import Deposit from './Deposit'; +import { useTranslation } from './translate'; +import Vote from './Vote'; + +interface Expanded { + ongoing: Referendum['info']['asOngoing']; + tallyTotal: BN; + shortHash: string; +} + +function expandOngoing (info: Referendum['info']): Expanded { + const ongoing = info.asOngoing; + const hexHash = ongoing.proposalHash.toHex(); + + return { + ongoing, + shortHash: `${hexHash.slice(0, 8)}…${hexHash.slice(-6)}`, + tallyTotal: ongoing.tally.ayes.add(ongoing.tally.nays) + }; +} + +function Ongoing ({ isMember, members, palletVote, value: { id, info } }: Props): React.ReactElement { + const { t } = useTranslation(); + const { ongoing: { decisionDeposit, proposalHash, submissionDeposit, tally }, shortHash, tallyTotal } = useMemo( + () => expandOngoing(info), + [info] + ); + const preimage = usePreimage(proposalHash); + + return ( + <> + + {preimage && preimage.proposal + ? ( + ('preimage')} + value={preimage.proposal} + withHash + /> + ) + : t('preimage {{shortHash}}', { replace: { shortHash } }) + } + + + + + + + {preimage && ( + + )} + + + ); +} + +export default React.memo(Ongoing); diff --git a/packages/page-referenda/src/RefOther.tsx b/packages/page-referenda/src/RefOther.tsx new file mode 100644 index 0000000000..e08550a7fe --- /dev/null +++ b/packages/page-referenda/src/RefOther.tsx @@ -0,0 +1,19 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ReferendumProps as Props } from './types'; + +import React from 'react'; + +function RefOther ({ value: { info: { type } } }: Props): React.ReactElement { + return ( + + {type} + + ); +} + +export default React.memo(RefOther); diff --git a/packages/page-referenda/src/RefTuple.tsx b/packages/page-referenda/src/RefTuple.tsx new file mode 100644 index 0000000000..8766a0293d --- /dev/null +++ b/packages/page-referenda/src/RefTuple.tsx @@ -0,0 +1,69 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option } from '@polkadot/types'; +import type { PalletReferendaDeposit } from '@polkadot/types/lookup'; +import type { Referendum, ReferendumProps as Props } from './types'; + +import React, { useMemo } from 'react'; + +import Deposit from './Deposit'; + +interface Expanded { + decision: Option | null; + submit: PalletReferendaDeposit | null; + when: Date | null; +} + +function expandTuple (info: Referendum['info']): Expanded { + const data = info.isApproved + ? info.asApproved + : info.isRejected + ? info.asRejected + : info.isCancelled + ? info.asCancelled + : info.isTimedOut + ? info.asTimedOut + : null; + + if (!data) { + return { + decision: null, + submit: null, + when: null + }; + } + + return { + decision: data[2], + submit: data[1], + when: new Date(data[0].toNumber()) + }; +} + +function Tuple ({ value: { info } }: Props): React.ReactElement { + const { decision, submit, when } = useMemo( + () => expandTuple(info), + [info] + ); + + return ( + <> + + + + {when && ( + when.toUTCString() + )} + + + {info.type} + + + ); +} + +export default React.memo(Tuple); diff --git a/packages/page-referenda/src/Referenda.tsx b/packages/page-referenda/src/Referenda.tsx new file mode 100644 index 0000000000..b24f590b63 --- /dev/null +++ b/packages/page-referenda/src/Referenda.tsx @@ -0,0 +1,58 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletReferenda, PalletVote } from './types'; + +import React, { useMemo, useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; +import { useAccounts } from '@polkadot/react-hooks'; + +import Referendum from './Referendum'; +import { useTranslation } from './translate'; +import useReferenda from './useReferenda'; + +interface Props { + className?: string; + members?: string[]; + palletReferenda: PalletReferenda; + palletVote: PalletVote; +} + +function Referenda ({ className, members, palletReferenda, palletVote }: Props): React.ReactElement { + const { t } = useTranslation(); + const { allAccounts } = useAccounts(); + const referenda = useReferenda(palletReferenda); + + const isMember = useMemo( + () => !members || allAccounts.some((a) => members.includes(a)), + [allAccounts, members] + ); + + const headerRef = useRef([ + [t('referenda'), 'start', 2], + [undefined, undefined, 3] + ]); + + return ( +
+ ('No referendums found')} + header={headerRef.current} + > + {referenda && referenda.map((r) => ( + + ))} +
+
+ ); +} + +export default React.memo(Referenda); diff --git a/packages/page-referenda/src/Referendum.tsx b/packages/page-referenda/src/Referendum.tsx new file mode 100644 index 0000000000..816263a5ef --- /dev/null +++ b/packages/page-referenda/src/Referendum.tsx @@ -0,0 +1,41 @@ +// Copyright 2017-2022 @polkadot/app-referenda authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ReferendumProps as Props } from './types'; + +import React, { useMemo } from 'react'; + +import { formatNumber } from '@polkadot/util'; + +import Ongoing from './RefOngoing'; +import Other from './RefOther'; + +const Components: Record> = { + Ongoing +}; + +function Referendum (props: Props): React.ReactElement { + const { className, value: { id, info } } = props; + + const Component = useMemo( + () => Components[info.type] || Other, + [info] + ); + + return ( + + +

{formatNumber(id)}

+ + {/* + + */} + + + ); +} + +export default React.memo(Referendum); diff --git a/packages/page-referenda/src/Vote.tsx b/packages/page-referenda/src/Vote.tsx new file mode 100644 index 0000000000..9e5dca2cc6 --- /dev/null +++ b/packages/page-referenda/src/Vote.tsx @@ -0,0 +1,113 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Preimage } from '@polkadot/app-preimages/types'; +import type { BN } from '@polkadot/util'; +import type { PalletVote } from './types'; + +import React, { useState } from 'react'; + +import { Button, ConvictionDropdown, Modal, ProposedAction, TxButton, VoteAccount, VoteValue } from '@polkadot/react-components'; +import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks'; + +import { useTranslation } from './translate'; + +interface Props { + id: BN; + isMember: boolean; + members?: string[]; + palletVote: PalletVote; + preimage: Preimage; +} + +function Voting ({ id, isMember, members, palletVote, preimage: { proposal } }: Props): React.ReactElement | null { + const { t } = useTranslation(); + const { api } = useApi(); + const { hasAccounts } = useAccounts(); + const [accountId, setAccountId] = useState(null); + const [balance, setBalance] = useState(); + const [conviction, setConviction] = useState(0); + const [isVotingOpen, toggleVoting] = useToggle(); + + if (!hasAccounts) { + return null; + } + + return ( + <> + {isVotingOpen && ( + ('Vote on proposal')} + onClose={toggleVoting} + size='large' + > + + ('If this proposal is passed, the changes will be applied via dispatch and the deposit returned.')}> + + + ('The vote will be recorded for this account. If another account delegated to this one, the delegated votes will also be counted.')}> + + + +

{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.')}

+ + } + > + + ('The conviction to use for this vote, with an appropriate lock period.')} + label={t('conviction')} + onChange={setConviction} + value={conviction} + /> +
+
+ + ('Vote Nay')} + onStart={toggleVoting} + params={ + [id, { Standard: { balance, vote: { aye: false, conviction } } }] + } + tx={api.tx[palletVote].vote} + /> + ('Vote Aye')} + onStart={toggleVoting} + params={ + [id, { Standard: { balance, vote: { aye: true, conviction } } }] + } + tx={api.tx[palletVote].vote} + /> + +
+ )} +