Introduce tracks for gov2 referenda (#8141)

* Introduce tracks for gov2 referenda

* Adjust

* Track name

* Submit dialog

* Start of origin selection

* Submission cont

* At/After blocks
This commit is contained in:
Jaco 2022-09-19 13:08:18 +03:00 committed by GitHub
parent 9798950283
commit fcdca155eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 270 additions and 20 deletions

View File

@ -63,6 +63,7 @@ function Propose ({ className = '', onClose }: Props): React.ReactElement<Props>
<Input
autoFocus
help={t<string>('The preimage hash of the proposal')}
isError={!isHashValid}
label={t<string>('preimage hash')}
onChange={_onChangeHash}
value={hash}

View File

@ -37,7 +37,7 @@ function Add ({ className, imageHash }: Props): React.ReactElement<Props> {
useEffect((): void => {
const encodedProposal = (proposal as SubmittableExtrinsic<'promise'>)?.method.toHex() || '';
// we currently don't have a constant exposed, or rather, maybe somwhere else...
// we currently don't have a constant exposed, or rather, maybe somewhere else...
// const storageFee = api.consts.democracy.preimageByteDeposit.mul(
// encodedProposal
// ? new BN((encodedProposal.length - 2) / 2)

View File

@ -15,7 +15,7 @@ const Components: Record<string, React.ComponentType<Props>> = {
};
function Referendum (props: Props): React.ReactElement<Props> {
const { className, value: { id, info } } = props;
const { className, value: { id, info, track } } = props;
const Component = useMemo(
() => Components[info.type] || Other,
@ -27,6 +27,9 @@ function Referendum (props: Props): React.ReactElement<Props> {
<td className='number'>
<h1>{formatNumber(id)}</h1>
</td>
<td className='number'>
{track && track.name.toString()}
</td>
{/* <td>
<textarea
rows={8}

View File

@ -0,0 +1,169 @@
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RawParam } from '@polkadot/react-params/types';
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { PalletReferenda } from '../types';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useBestNumber } from '@polkadot/react-hooks';
import Params from '@polkadot/react-params';
import { Available } from '@polkadot/react-query';
import { getTypeDef } from '@polkadot/types/create';
import { formatNumber, isHex } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
className?: string;
members?: string[];
onClose: () => void;
palletReferenda: PalletReferenda;
tracks: [BN, PalletReferendaTrackInfo][];
}
interface HashState {
hash?: string;
isHashValid: boolean;
}
function Submit ({ className = '', members, onClose, palletReferenda }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const bestNumber = useBestNumber();
const [accountId, setAccountId] = useState<string | null>(null);
// const [track, setTrack] = useState<number | undefined>();
const [origin, setOrigin] = useState<RawParam['value'] | null>(null);
const [atAfter, setAtAfter] = useState<RawParam['value'] | null>(null);
const [{ hash, isHashValid }, setHash] = useState<HashState>({ hash: '', isHashValid: false });
const isInvalidAt = useMemo(
() => !bestNumber || !atAfter || (
(atAfter as { At: BN }).At
? (atAfter as { At: BN }).At.lte(bestNumber)
: (atAfter as { After: BN }).After.lte(bestNumber)
),
[atAfter, bestNumber]
);
const [originType, atAfterType] = useMemo(
() => [
[{
type: getTypeDef(api.tx[palletReferenda as 'referenda'].submit.meta.args[0].type.toString())
}],
[{
type: getTypeDef(api.tx[palletReferenda as 'referenda'].submit.meta.args[2].type.toString())
}]
],
[api, palletReferenda]
);
// Idially we would just like to use the track - need a mapping for these
// const trackOpts = useMemo(
// () => tracks.map(([id, track]) => ({
// text: track.name.toString(),
// value: id.toNumber()
// })),
// [tracks]
// );
const _onChangeOrigin = useCallback(
([{ isValid, value }]: RawParam[]) =>
setOrigin(isValid ? value : null),
[]
);
const _onChangeAtAfter = useCallback(
([{ isValid, value }]: RawParam[]) =>
setAtAfter(isValid ? value : null),
[]
);
const _onChangeHash = useCallback(
(hash?: string) =>
setHash({ hash, isHashValid: isHex(hash, 256) }),
[]
);
console.log(atAfter);
return (
<Modal
className={className}
header={t<string>('Submit proposal')}
onClose={onClose}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t<string>('The proposal will be registered from this account and the balance lock will be applied here.')}>
<InputAddress
filter={members}
help={t<string>('The account you want to register the proposal from')}
label={t<string>('send from account')}
labelExtra={
<Available
label={<span className='label'>{t<string>('transferrable')}</span>}
params={accountId}
/>
}
onChange={setAccountId}
type='account'
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The hash of the preimage for the proposal as previously submitted or intended.')}>
<Input
autoFocus
help={t<string>('The preimage hash of the proposal')}
isError={!isHashValid}
label={t<string>('preimage hash')}
onChange={_onChangeHash}
value={hash}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The origin (and by extension track) that you wish to submit for, each has a different period, different root and acceptance criteria.')}>
{/* <Dropdown
label={t<string>('submission track')}
onChange={setTrack}
options={trackOpts}
/> */}
<Params
className='originSelect'
onChange={_onChangeOrigin}
params={originType}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The moment of enactment, either at a specific block, or after a specific block. Currently at #{{bestNumber}}, the selected block should be after the current best.', { replace: { bestNumber: formatNumber(bestNumber) } })}>
<Params
className='timeSelect'
isError={isInvalidAt}
onChange={_onChangeAtAfter}
params={atAfterType}
/>
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={accountId}
icon='plus'
isDisabled={!origin || !atAfter || !isHashValid || !accountId || isInvalidAt}
label={t<string>('Submit proposal')}
onStart={onClose}
params={[origin, hash, atAfter]}
tx={api.tx[palletReferenda as 'referenda'].submit}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(styled(Submit)`
.originSelect, .timeSelect {
> .ui--Params-Content {
padding-left: 0;
}
}
`);

View File

@ -40,7 +40,7 @@ function Votes ({ className = '', isConvictionVote, tally }: Props): React.React
summary={
<>
{t<string>('Nay')}
<div><FormatBalance value={tally.ayes} /></div>
<div><FormatBalance value={tally.nays} /></div>
</>
}
/>

View File

@ -5,13 +5,15 @@ 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 AddPreimage from '@polkadot/app-preimages/Preimages/Add';
import { Button, Table } from '@polkadot/react-components';
import { useAccounts, useToggle } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import useReferenda from '../useReferenda';
import useSummary from '../useSummary';
import Referendum from './Referendum';
import Submit from './Submit';
import Summary from './Summary';
export { useCounterNamed as useCounter } from '../useCounter';
@ -26,8 +28,9 @@ interface Props {
function Referenda ({ className, members, palletReferenda, palletVote }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const referenda = useReferenda(palletReferenda);
const [referenda, tracks] = useReferenda(palletReferenda);
const summary = useSummary(palletReferenda, referenda);
const [isSubmitOpen, toggleSubmit] = useToggle();
const isMember = useMemo(
() => !members || allAccounts.some((a) => members.includes(a)),
@ -42,6 +45,27 @@ function Referenda ({ className, members, palletReferenda, palletVote }: Props):
return (
<div className={className}>
<Summary summary={summary} />
<Button.Group>
<AddPreimage />
{tracks && (
<>
<Button
icon='plus'
isDisabled={!isMember}
label={t<string>('Submit proposal')}
onClick={toggleSubmit}
/>
{isSubmitOpen && (
<Submit
members={members}
onClose={toggleSubmit}
palletReferenda={palletReferenda}
tracks={tracks}
/>
)}
</>
)}
</Button.Group>
<Table
className={className}
empty={referenda && t<string>('No referendums found')}

View File

@ -1,7 +1,7 @@
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally } from '@polkadot/types/lookup';
import type { PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally, PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
export type PalletReferenda = 'referenda' | 'rankedPolls' | 'fellowshipReferenda';
@ -13,6 +13,7 @@ export interface Referendum {
info: PalletReferendaReferendumInfoConvictionVotingTally | PalletReferendaReferendumInfoRankedCollectiveTally;
isConvictionVote: boolean;
key: string;
track?: PalletReferendaTrackInfo;
}
export interface ReferendumProps {

View File

@ -10,11 +10,11 @@ import { createNamedHook } from '@polkadot/react-hooks';
import useReferenda from './useReferenda';
export function useCounterNamed (palletReferenda: PalletReferenda): number {
const refs = useReferenda(palletReferenda);
const [referanda] = useReferenda(palletReferenda);
return useMemo(
() => (refs && refs.filter(({ info }) => info.isOngoing).length) || 0,
[refs]
() => (referanda && referanda.filter(({ info }) => info.isOngoing).length) || 0,
[referanda]
);
}

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@polkadot/types';
import type { PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally } from '@polkadot/types/lookup';
import type { PalletReferendaReferendumInfoConvictionVotingTally, PalletReferendaReferendumInfoRankedCollectiveTally, PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { PalletReferenda, Referendum } from './types';
@ -11,6 +11,7 @@ import { useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import useReferendaIds from './useReferendaIds';
import useTracks from './useTracks';
const ORDER = <const> ['Ongoing', 'Approved', 'Rejected', 'Cancelled', 'Killed', 'TimedOut'];
@ -64,16 +65,38 @@ const OPT_MULTI = {
withParamsTransform: true
};
function useReferendaImpl (palletReferenda: PalletReferenda): Referendum[] | undefined {
function getResult (referenda: Referendum[], tracks?: [BN, PalletReferendaTrackInfo][]): Referendum[] | undefined {
if (tracks) {
for (let i = 0; i < referenda.length; i++) {
if (referenda[i].info.isOngoing) {
const track = tracks.find(([id]) => id.eq(referenda[i].info.asOngoing.track));
if (track) {
referenda[i].track = track[1];
}
}
}
}
return referenda;
}
function useReferendaImpl (palletReferenda: PalletReferenda): [Referendum[] | undefined, [BN, PalletReferendaTrackInfo][] | undefined] {
const { api, isApiReady } = useApi();
const ids = useReferendaIds(palletReferenda);
const tracks = useTracks(palletReferenda);
const referenda = useCall(isApiReady && ids && ids.length !== 0 && api.query[palletReferenda].referendumInfoFor.multi, [ids], OPT_MULTI);
return useMemo(
() => ids && ids.length === 0
? []
: referenda,
[ids, referenda]
() => [
(ids && ids.length === 0)
? []
: referenda
? getResult(referenda, tracks)
: undefined,
tracks
],
[ids, referenda, tracks]
);
}

View File

@ -0,0 +1,23 @@
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { PalletReferenda } from './types';
import { useMemo } from 'react';
import { createNamedHook, useApi } from '@polkadot/react-hooks';
function useTracksImpl (palletReferenda: PalletReferenda): [BN, PalletReferendaTrackInfo][] | undefined {
const { api, isApiReady } = useApi();
return useMemo(
() => isApiReady
? api.consts[palletReferenda as 'referenda'] && api.consts[palletReferenda as 'referenda'].tracks
: undefined,
[api, isApiReady, palletReferenda]
);
}
export default createNamedHook('useTraqcks', useTracksImpl);

View File

@ -66,7 +66,7 @@ export default React.memo(styled(ProposedAction)`
margin-left: 2rem;
.ui--ProposedAction-extrinsic {
.ui--Params-Content {
> .ui--Params-Content {
padding-left: 0;
}
}

View File

@ -127,6 +127,7 @@ function EnumParam (props: Props): React.ReactElement<Props> {
/>
{current && (
<Params
isError={isError}
onChange={_onChangeParam}
overrides={overrides}
params={current}

View File

@ -23,7 +23,7 @@ function formatJSON (input: string): string {
.replace(/,/g, ', ');
}
function Param ({ className = '', defaultValue, isDisabled, isInOption, isOptional, name, onChange, onEnter, onEscape, overrides, registry, type }: Props): React.ReactElement<Props> | null {
function Param ({ className = '', defaultValue, isDisabled, isError, isInOption, isOptional, name, onChange, onEnter, onEscape, overrides, registry, type }: Props): React.ReactElement<Props> | null {
const Component = useMemo(
() => findComponent(registry, type, overrides),
[registry, type, overrides]
@ -62,6 +62,7 @@ function Param ({ className = '', defaultValue, isDisabled, isInOption, isOption
className={`ui--Param ${className}`}
defaultValue={defaultValue}
isDisabled={isDisabled}
isError={isError}
isInOption={isInOption}
key={`${name || 'unknown'}:${label}`}
label={label}

View File

@ -12,6 +12,7 @@ interface Props {
defaultValue: RawParam;
index: number;
isDisabled?: boolean;
isError?: boolean;
name?: string;
onChange: (index: number, value: RawParamOnChangeValue) => void;
onEnter?: () => void;
@ -22,7 +23,7 @@ interface Props {
values?: RawParams | null;
}
function ParamComp ({ defaultValue, index, isDisabled, name, onChange, onEnter, onEscape, overrides, registry, type }: Props): React.ReactElement<Props> {
function ParamComp ({ defaultValue, index, isDisabled, isError, name, onChange, onEnter, onEscape, overrides, registry, type }: Props): React.ReactElement<Props> {
const _onChange = useCallback(
(value: RawParamOnChangeValue): void =>
onChange(index, value),
@ -34,6 +35,7 @@ function ParamComp ({ defaultValue, index, isDisabled, name, onChange, onEnter,
<Param
defaultValue={defaultValue}
isDisabled={isDisabled}
isError={isError}
key={`input:${index}`}
name={name}
onChange={_onChange}

View File

@ -19,6 +19,7 @@ import { createValue } from './values';
interface Props extends I18nProps {
children?: React.ReactNode;
isDisabled?: boolean;
isError?: boolean;
onChange?: (value: RawParams) => void;
onEnter?: () => void;
onError?: () => void;
@ -80,7 +81,7 @@ class Params extends React.PureComponent<Props, State> {
}
public override render (): React.ReactNode {
const { children, className = '', isDisabled, onEnter, onEscape, overrides, params, registry = api.registry, withBorder = true } = this.props;
const { children, className = '', isDisabled, isError, onEnter, onEscape, overrides, params, registry = api.registry, withBorder = true } = this.props;
const { values = this.props.values } = this.state;
if (!values || !values.length) {
@ -99,6 +100,7 @@ class Params extends React.PureComponent<Props, State> {
defaultValue={values[index]}
index={index}
isDisabled={isDisabled}
isError={isError}
key={`${name || ''}:${type.type.toString()}:${index}:${isDisabled ? stringify(values[index]) : ''}`}
name={name}
onChange={this.onChangeParam}