Skip to content

Commit

Permalink
Slash revert council proposal (polkadot-js#2093)
Browse files Browse the repository at this point in the history
* Slash revert council proposal

* Disabled slashes when none
  • Loading branch information
jacogr authored and KarishmaBothara committed Feb 20, 2020
1 parent 907e6c9 commit 794091b
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 11 deletions.
16 changes: 7 additions & 9 deletions packages/app-council/src/Motions/Propose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,13 @@ class Propose extends TxModal<Props, State> {
const { isMember, t } = this.props;

return (
<Button.Group>
<Button
isDisabled={!isMember}
isPrimary
label={t('Propose a council motion')}
icon='add'
onClick={this.showModal}
/>
</Button.Group>
<Button
isDisabled={!isMember}
isPrimary
label={t('Propose motion')}
icon='add'
onClick={this.showModal}
/>
);
}

Expand Down
143 changes: 143 additions & 0 deletions packages/app-council/src/Motions/Slashing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2017-2020 @polkadot/ui-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { AccountId } from '@polkadot/types/interfaces';
import { CallFunction } from '@polkadot/types/types';

import React, { useEffect, useState } from 'react';
import { Button, Dropdown, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';

import { useTranslation } from '../translate';
import useAvailableSlashes from './useAvailableSlashes';

interface Props {
className?: string;
isMember: boolean;
}

interface Option {
text: string;
value: number;
}

export default function Slashing ({ className, isMember }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const members = useCall<AccountId[]>(api.query.electionsPhragmen?.members || api.query.elections.members, []);
const slashes = useAvailableSlashes();
const [isVisible, toggleVisible] = useToggle();
const [isSelectedMember, setIsMember] = useState(false);
const [accountId, setAcountId] = useState<string | null>(null);
const [proposal, setProposal] = useState<CallFunction | null>(null);
const [eras, setEras] = useState<Option[]>([]);
const [selectedEra, setSelectedEra] = useState(0);
const threshold = Math.ceil((members?.length || 0) * 0.75);

useEffect((): void => {
if (accountId && members) {
setIsMember(
members
.map(([accountId]): string => accountId.toString())
.includes(accountId)
);
}
}, [accountId, members]);

useEffect((): void => {
if (slashes?.length) {
setEras(
slashes.map(([era, slashes]): Option => ({
text: t('era {{era}}, {{count}} slashes', {
replace: {
count: slashes.length,
era: era.toNumber()
}
}),
value: era.toNumber()
}))
);
} else {
setEras([]);
}
}, [slashes]);

useEffect((): void => {
const actioned = selectedEra && slashes?.find(([era]): boolean => era.eqn(selectedEra));

setProposal((): any =>
actioned
? api.tx.staking.cancelDeferredSlash(actioned[0], actioned[1].map((_, index): number => index))
: null
);
}, [selectedEra, slashes]);

return (
<>
<Button
icon='cancel'
isDisabled={!isMember || !members?.length || !slashes.length}
isPrimary
label={t('Cancel slashes')}
onClick={toggleVisible}
/>
{isVisible && (
<Modal
className={className}
header={t('Revert pending slashes')}
open
>
<Modal.Content>
<InputAddress
help={t('Select the account you wish to make the proposal with.')}
label={t('propose from account')}
onChange={setAcountId}
type='account'
withLabel
/>
{eras.length
? (
<Dropdown
defaultValue={eras[0].value}
help={t('The unapplied slashed era to cancel.')}
label={t('the era to cancel for')}
onChange={setSelectedEra}
options={eras}
/>
)
: (
<Input
isDisabled
label={t('the era to cancel for')}
value={t('no unapplied slashes found')}
/>
)
}
</Modal.Content>
<Modal.Actions>
<Button.Group>
<Button
isNegative
icon='cancel'
label={t('Cancel')}
onClick={toggleVisible}
/>
<Button.Or />
<TxButton
accountId={accountId}
icon='repeat'
isDisabled={!threshold || !isSelectedMember || !proposal}
isPrimary
label={t('Revert')}
onStart={toggleVisible}
params={[threshold, proposal]}
tx='council.propose'
/>
</Button.Group>
</Modal.Actions>
</Modal>
)}
</>
);
}
9 changes: 7 additions & 2 deletions packages/app-council/src/Motions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { I18nProps } from '@polkadot/react-components/types';
import { AccountId, Balance } from '@polkadot/types/interfaces';

import React, { useEffect, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { Button, Table } from '@polkadot/react-components';
import { useApi, useAccounts, useCall } from '@polkadot/react-hooks';

import Motion from './Motion';
import Propose from './Propose';
import Slashing from './Slashing';
import translate from '../translate';

interface Props extends I18nProps {
Expand All @@ -36,7 +37,11 @@ function Proposals ({ className, motions, t }: Props): React.ReactElement<Props>

return (
<div className={className}>
<Propose isMember={isMember} />
<Button.Group>
<Propose isMember={isMember} />
<Button.Or />
<Slashing isMember={isMember} />
</Button.Group>
{motions?.length
? (
<Table>
Expand Down
53 changes: 53 additions & 0 deletions packages/app-council/src/Motions/useAvailableSlashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2017-2020 @polkadot/ui-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { EraIndex, UnappliedSlash } from '@polkadot/types/interfaces';

import BN from 'bn.js';
import { useEffect, useState } from 'react';
import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks';
import { Option, Vec } from '@polkadot/types';

type Unsub = () => void;

export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] {
const { api } = useApi();
const currentEra = useCall<EraIndex>(api.query.staking?.currentEra, []);
const earliestSlash = useCall<Option<EraIndex>>(api.query.staking?.earliestUnappliedSlash, []);
const mounted = useIsMountedRef();
const [slashes, setSlashes] = useState<[BN, UnappliedSlash[]][]>([]);

useEffect((): Unsub => {
let unsub: Unsub | undefined;

if (mounted.current && currentEra && earliestSlash?.isSome) {
const from = earliestSlash.unwrap();
const range: BN[] = [];
let start = new BN(from);

while (start.lt(currentEra)) {
range.push(start);
start = start.addn(1);
}

if (range.length) {
(async (): Promise<void> => {
unsub = await api.query.staking.unappliedSlashes.multi<Vec<UnappliedSlash>>(range, (values): void =>
setSlashes(
values
.map((value, index): [BN, UnappliedSlash[]] => [from.addn(index), value])
.filter(([, slashes]): boolean => slashes.length !== 0)
)
);
})();
}
}

return (): void => {
unsub && unsub();
};
}, [currentEra, earliestSlash]);

return slashes;
}

0 comments on commit 794091b

Please sign in to comment.