Bounties - add slash curator action (#4499)
This commit is contained in:
parent
9be16b540d
commit
55ab547c86
@ -21,11 +21,16 @@
|
||||
"Curator has been proposed by council. The bounty is waiting for curator to accept the role.": "Curator has been proposed by council. The bounty is waiting for curator to accept the role.",
|
||||
"Curator under voting": "Curator under voting",
|
||||
"Curator's deposit": "Curator's deposit",
|
||||
"Curator's deposit will be slashed and curator will be unassigned. Bounty will return to the Funded state.": "Curator's deposit will be slashed and curator will be unassigned. Bounty will return to the Funded state.",
|
||||
"Curator's fee": "Curator's fee",
|
||||
"Curator's fee can't be higher than bounty value.": "Curator's fee can't be higher than bounty value.",
|
||||
"Description of the Bounty (to be stored on-chain)": "Description of the Bounty (to be stored on-chain)",
|
||||
"Extend Expiry": "Extend Expiry",
|
||||
"Give Up": "Give Up",
|
||||
"Give up curator's role": "Give up curator's role",
|
||||
"How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.": "How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.",
|
||||
"If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.": "If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.",
|
||||
"If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.": "If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.",
|
||||
"Initiate Voting": "Initiate Voting",
|
||||
"Nay": "Nay",
|
||||
"Nay: {{count}}": "Nay: {{count}}",
|
||||
@ -42,8 +47,16 @@
|
||||
"Select the account you wish to propose the bounty from.": "Select the account you wish to propose the bounty from.",
|
||||
"Select the council account you wish to use to create a motion for the Bounty.": "Select the council account you wish to use to create a motion for the Bounty.",
|
||||
"Select the council account you wish to use to create a proposal for closing bounty.": "Select the council account you wish to use to create a proposal for closing bounty.",
|
||||
"Slash Curator": "Slash Curator",
|
||||
"Slash curator": "Slash curator",
|
||||
"The Curator account that will give up on it's role": "The Curator account that will give up on it's role",
|
||||
"The Curator that will be slashed.": "The Curator that will be slashed.",
|
||||
"The Curator that will be unassigned": "The Curator that will be unassigned",
|
||||
"The account that will create the transaction.": "The account that will create the transaction.",
|
||||
"The account that will sign the transaction.": "The account that will sign the transaction.",
|
||||
"The council member that will create a motion, submission equates to an \"aye\" vote for chosen option.": "The council member that will create a motion, submission equates to an \"aye\" vote for chosen option.",
|
||||
"The council member that will create the close bounty proposal, submission equates to an \"aye\" vote.": "The council member that will create the close bounty proposal, submission equates to an \"aye\" vote.",
|
||||
"The council member that will create the motion, submission equates to an \"aye\" vote.": "The council member that will create the motion, submission equates to an \"aye\" vote.",
|
||||
"The council member that will create the motion.": "The council member that will create the motion.",
|
||||
"The description of this bounty": "The description of this bounty",
|
||||
"The note on the extension.": "The note on the extension.",
|
||||
@ -51,21 +64,25 @@
|
||||
"The total payment amount of this bounty, curator's fee included.": "The total payment amount of this bounty, curator's fee included.",
|
||||
"This account will be used to create an extend bounty expire transaction.": "This account will be used to create an extend bounty expire transaction.",
|
||||
"This account will propose the bounty. Bond amount will be reserved on its balance.": "This account will propose the bounty. Bond amount will be reserved on its balance.",
|
||||
"This action will Slash the Curator.": "This action will Slash the Curator.",
|
||||
"This action will create a Council motion to assign a Curator.": "This action will create a Council motion to assign a Curator.",
|
||||
"This action will create a Council motion to either approve or reject the Bounty.": "This action will create a Council motion to either approve or reject the Bounty.",
|
||||
"This action will create a Council motion to slash the Curator.": "This action will create a Council motion to slash the Curator.",
|
||||
"This action will create a Council motion to unassign the Curator.": "This action will create a Council motion to unassign the Curator.",
|
||||
"This action will create a Council proposal to close the Bounty.": "This action will create a Council proposal to close the Bounty.",
|
||||
"This action will extend expiry time of the selected bounty.": "This action will extend expiry time of the selected bounty.",
|
||||
"This action will unassign you from a curator role.": "This action will unassign you from a curator role.",
|
||||
"This amount will be reserved from origin account and returned on approval or slashed upon rejection.": "This amount will be reserved from origin account and returned on approval or slashed upon rejection.",
|
||||
"This bounty has received approval and is currently being implemented.": "This bounty has received approval and is currently being implemented.",
|
||||
"This bounty is approved and funded.": "This bounty is approved and funded.",
|
||||
"This bounty was approved by the council. When next treasury spend period comes, it will get funded.": "This bounty was approved by the council. When next treasury spend period comes, it will get funded.",
|
||||
"This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.": "This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.",
|
||||
"Title too long": "Title too long",
|
||||
"Unassign Curator": "Unassign Curator",
|
||||
"Unassign curator": "Unassign curator",
|
||||
"Unassign curator under voting": "Unassign curator under voting",
|
||||
"Value": "Value",
|
||||
"You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.": "You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.",
|
||||
"active": "active",
|
||||
"beneficiary": "beneficiary",
|
||||
"bounties": "bounties",
|
||||
"bounty bond": "bounty bond",
|
||||
"bounty remark": "bounty remark",
|
||||
"bounty requested allocation": "bounty requested allocation",
|
||||
@ -74,11 +91,11 @@
|
||||
"curator": "curator",
|
||||
"curator account": "curator account",
|
||||
"curator's fee": "curator's fee",
|
||||
"current curator": "current curator",
|
||||
"next action": "next action",
|
||||
"next bounty funding in": "next bounty funding in",
|
||||
"next burn": "next burn",
|
||||
"past": "past",
|
||||
"payout due": "payout due",
|
||||
"propose with account": "propose with account",
|
||||
"proposing account": "proposing account",
|
||||
"select curator": "select curator",
|
||||
@ -87,7 +104,6 @@
|
||||
"submit with account": "submit with account",
|
||||
"title": "title",
|
||||
"treasury": "treasury",
|
||||
"update due": "update due",
|
||||
"value": "value",
|
||||
"vote with account": "vote with account",
|
||||
"{{blocks}} blocks": "{{blocks}} blocks"
|
||||
|
||||
@ -26,7 +26,6 @@
|
||||
"Change democracy delegation": "",
|
||||
"Change this account's password": "",
|
||||
"Claim": "",
|
||||
"Claimable": "",
|
||||
"Clear expired democracy locks": "",
|
||||
"Close": "",
|
||||
"Contracts": "",
|
||||
@ -35,6 +34,7 @@
|
||||
"Curator has been proposed by council. The bounty is waiting for curator to accept the role.": "",
|
||||
"Curator under voting": "",
|
||||
"Curator's deposit": "",
|
||||
"Curator's deposit will be slashed and curator will be unassigned. Bounty will return to the Funded state.": "",
|
||||
"Curator's fee": "",
|
||||
"Dark theme (experimental, work-in-progress)": "",
|
||||
"Delegate democracy votes": "",
|
||||
@ -50,7 +50,11 @@
|
||||
"For final approvals, the actual full call data is required to execute the transaction": "",
|
||||
"Forget this account": "",
|
||||
"Full Legal Name": "",
|
||||
"Give Up": "",
|
||||
"Give up curator's role": "",
|
||||
"Governance": "",
|
||||
"If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.": "",
|
||||
"If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.": "",
|
||||
"If you have not claimed rewards straight after the end of the era, the validator is in the active set and you are seeing no rewards, this would mean that the reward payout transaction was made by another account on your behalf. Always check your favorite explorer to see any historic payouts made to your accounts.": "",
|
||||
"Inactive": "",
|
||||
"Initiate recovery for another": "",
|
||||
@ -90,6 +94,8 @@
|
||||
"Set on-chain sub-identities": "",
|
||||
"Settings": "",
|
||||
"Show address on hardware device": "",
|
||||
"Slash Curator": "",
|
||||
"Slash curator": "",
|
||||
"Slashed": "",
|
||||
"Specified payment account": "",
|
||||
"Specify the user account to use for this deployment. Any fees will be deducted from this account.": "",
|
||||
@ -103,6 +109,10 @@
|
||||
"Stash account (increase the amount at stake)": "",
|
||||
"Submit (no signature)": "",
|
||||
"Swap to a non-executing approval type, with subsequent calls providing the actual call data.": "",
|
||||
"The Curator account that will give up on it's role": "",
|
||||
"The Curator that will be slashed.": "",
|
||||
"The Curator that will be unassigned": "",
|
||||
"The account that will create the transaction.": "",
|
||||
"The account type (derivation) to use": "",
|
||||
"The account type that you wish to create. This is the top-level derivation.": "",
|
||||
"The actual fully constructed signed output. This can be used for submission via other channels.": "",
|
||||
@ -117,6 +127,7 @@
|
||||
"The code hash for the on-chain deployed code.": "",
|
||||
"The codeHash is not a valid hex hash": "",
|
||||
"The controller does no have sufficient funds available to cover transaction fees. Ensure that a funded controller is used.": "",
|
||||
"The council member that will create the motion, submission equates to an \"aye\" vote.": "",
|
||||
"The maximum amount you can delegate is the amount of funds available on the delegating account.": "",
|
||||
"The multisig signatory for this transaction.": "",
|
||||
"The operation type to apply. For approvals both non-final and final approvals are supported.": "",
|
||||
@ -125,6 +136,10 @@
|
||||
"The sending account that will be used to send this transaction. Any applicable fees will be paid by this account.": "",
|
||||
"The signatory is one of the allowed accounts on the multisig, making a recorded approval for the transaction.": "",
|
||||
"The specified value is too large and does not allow funds to pay future transaction fees.": "",
|
||||
"This action will Slash the Curator.": "",
|
||||
"This action will create a Council motion to slash the Curator.": "",
|
||||
"This action will create a Council motion to unassign the Curator.": "",
|
||||
"This action will unassign you from a curator role.": "",
|
||||
"This bounty has received approval and is currently being implemented.": "",
|
||||
"This bounty is approved and funded.": "",
|
||||
"This bounty was approved by the council. When next treasury spend period comes, it will get funded.": "",
|
||||
@ -135,6 +150,8 @@
|
||||
"This vote does not affect any economics of the Polkadot platform. Staking rewards, inflation, effective market capitalisation and the underlying balances of every account remain completely unchanged. It is \"merely\" about what units we use to denominate the balances into \"DOT\" for the purpose of display.": "",
|
||||
"Unable to find on-chain WASM code for the supplied codeHash": "",
|
||||
"Unable to retrieve the specified block details. {{error}}": "",
|
||||
"Unassign Curator": "",
|
||||
"Unassign curator": "",
|
||||
"Unassign curator under voting": "",
|
||||
"Undelegate": "",
|
||||
"Unknown": "",
|
||||
@ -144,7 +161,6 @@
|
||||
"Upload": "",
|
||||
"Upload WASM": "",
|
||||
"Validators": "",
|
||||
"Value": "",
|
||||
"Vote": "",
|
||||
"Voting": "",
|
||||
"Voting costs nothing other than the transaction fee and can be done from all accounts with a non-zero spendable balance.": "",
|
||||
@ -152,6 +168,7 @@
|
||||
"Warning: we did not find any attest statement for {{chain}}": "",
|
||||
"With the keep-alive option set, the account is protected against removal due to low balances.": "",
|
||||
"Yes": "",
|
||||
"You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.": "",
|
||||
"You need to sign an attestation for the following account:": "",
|
||||
"You need to sign an attestation for the following accounts:": "",
|
||||
"account type": "",
|
||||
@ -172,7 +189,6 @@
|
||||
"bids": "",
|
||||
"bond": "",
|
||||
"bonded": "",
|
||||
"bounties": "",
|
||||
"call data for final approval": "",
|
||||
"candidates": "",
|
||||
"chain best": "",
|
||||
@ -232,7 +248,6 @@
|
||||
"parent": "",
|
||||
"payment": "",
|
||||
"payout": "",
|
||||
"payout due": "",
|
||||
"payout/stash": "",
|
||||
"payout/validator": "",
|
||||
"per block": "",
|
||||
@ -274,7 +289,6 @@
|
||||
"unapplied": "",
|
||||
"until block": "",
|
||||
"upcoming": "",
|
||||
"update due": "",
|
||||
"upgrade": "",
|
||||
"validation": "",
|
||||
"validator {{index}}": "",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '@polkadot/react-components/i18n';
|
||||
|
||||
@ -25,23 +25,17 @@ import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFa
|
||||
import { aliceSigner, MemoryStore } from '@polkadot/test-support/keyring';
|
||||
import { TypeRegistry } from '@polkadot/types/create';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { extractTime } from '@polkadot/util';
|
||||
|
||||
import { alice,
|
||||
bob,
|
||||
defaultAccounts,
|
||||
defaultBalance,
|
||||
defaultBountyApi,
|
||||
defaultMembers,
|
||||
defaultTreasury,
|
||||
ferdie } from '../test/hooks/defaults';
|
||||
import { alice, bob, defaultBalance, defaultBountyApi, defaultMembers, defaultTreasury, ferdie } from '../test/hooks/defaults';
|
||||
import Bounties from './Bounties';
|
||||
import { BountyApi } from './hooks';
|
||||
|
||||
const mockMembers = defaultMembers;
|
||||
const mockAccounts = defaultAccounts;
|
||||
const mockTreasury = defaultTreasury;
|
||||
let mockBountyApi = defaultBountyApi;
|
||||
let mockBalance = defaultBalance;
|
||||
const mockBlockTime = [50, '', extractTime(1)];
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useBalance: () => mockBalance,
|
||||
@ -56,8 +50,8 @@ jest.mock('@polkadot/react-hooks/useMembers', () => ({
|
||||
useMembers: () => mockMembers
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useAccounts', () => ({
|
||||
useAccounts: () => mockAccounts
|
||||
jest.mock('@polkadot/react-hooks/useBlockTime', () => ({
|
||||
useBlockTime: () => mockBlockTime
|
||||
}));
|
||||
|
||||
function aGenesisHash () {
|
||||
@ -86,13 +80,14 @@ let aBountyStatus: (status: string) => BountyStatus;
|
||||
let aBountyIndex: (index?:number) => BountyIndex;
|
||||
let aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
|
||||
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
let bountyStatusWith: ({ curator, status }: { curator?: string, status?: string, }) => BountyStatus;
|
||||
|
||||
describe('Bounties', () => {
|
||||
beforeAll(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
|
||||
augmentedApi = createAugmentedApi();
|
||||
({ aBounty, aBountyIndex, aBountyStatus, bountyWith } = new BountyFactory(augmentedApi));
|
||||
({ aBounty, aBountyIndex, aBountyStatus, bountyStatusWith, bountyWith } = new BountyFactory(augmentedApi));
|
||||
});
|
||||
beforeEach(() => {
|
||||
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
|
||||
@ -337,7 +332,7 @@ describe('Bounties', () => {
|
||||
|
||||
describe('extend bounty expiry action modal', () => {
|
||||
it('queues extend bounty expiry extrinsic on submit', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
const { findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
@ -354,11 +349,59 @@ describe('Bounties', () => {
|
||||
const acceptButton = await findByText('Accept');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM', extrinsic: 'mockProposeExtrinsic' }));
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockExtendExtrinsic' }));
|
||||
expect(mockBountyApi.extendBountyExpiry).toHaveBeenCalledWith(aBountyIndex(0), 'The bounty extend expiry remark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slash curator action modal', () => {
|
||||
it('give up on a Curator function in Active state bounty', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
const { findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
const slashCuratorButton = await findByText('Give Up');
|
||||
|
||||
fireEvent.click(slashCuratorButton);
|
||||
|
||||
expect(await findByText('This action will unassign you from a curator role.')).toBeTruthy();
|
||||
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
|
||||
const acceptButton = await findByText('Approve');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockUnassignExtrinsic' }));
|
||||
});
|
||||
|
||||
it('for bounty in PendingPayout state', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const { findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
const slashCuratorButton = await findByText('Slash Curator');
|
||||
|
||||
fireEvent.click(slashCuratorButton);
|
||||
|
||||
expect(await findByText('This action will create a Council motion to slash the Curator.')).toBeTruthy();
|
||||
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
|
||||
const acceptButton = await findByText('Approve');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('status is extended', () => {
|
||||
it('on proposed curator', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
|
||||
@ -10,8 +10,8 @@ import { useBounties } from '@polkadot/app-bounties/hooks';
|
||||
import { TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import { isClaimable } from './helpers';
|
||||
import { useTranslation } from './translate';
|
||||
import { isClaimable } from '../helpers';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
beneficiaryId: AccountId;
|
||||
@ -5,11 +5,11 @@ import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useBounties } from '@polkadot/app-bounties/hooks';
|
||||
import { TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from './translate';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
@ -11,8 +11,8 @@ import { getTreasuryProposalThreshold } from '@polkadot/apps-config';
|
||||
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useApi, useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
index: BountyIndex;
|
||||
@ -11,8 +11,8 @@ import { getTreasuryProposalThreshold } from '@polkadot/apps-config';
|
||||
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useApi, useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
index: BountyIndex;
|
||||
@ -53,13 +53,9 @@ function CloseBounty ({ index, proposals }: Props): React.ReactElement<Props> |
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will create a Council proposal to close the Bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will create a Council proposal to close the Bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
@ -8,9 +8,9 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { truncateTitle } from './helpers';
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
import { truncateTitle } from '../helpers';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
description: string
|
||||
@ -51,11 +51,11 @@ function ExtendBountyExpiryAction ({ curatorId, description, index }: Props): Re
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={curatorId.toString()}
|
||||
help={t<string>('This account will be used to create an extend bounty expire transaction.')}
|
||||
isDisabled
|
||||
label={t<string>('curator account')}
|
||||
type='account'
|
||||
value={curatorId.toString()}
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
@ -13,8 +13,8 @@ import { Button, InputAddress, InputBalance, MarkError, Modal, TxButton } from '
|
||||
import { useApi, useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
description: string
|
||||
186
packages/page-bounties/src/BountyActions/SlashCurator.tsx
Normal file
186
packages/page-bounties/src/BountyActions/SlashCurator.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { AccountId, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import { getTreasuryProposalThreshold } from '@polkadot/apps-config';
|
||||
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { determineUnassignCuratorAction, truncateTitle } from '../helpers';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useUserRole } from '../hooks/useUserRole';
|
||||
import { useTranslation } from '../translate';
|
||||
import { ValidUnassignCuratorAction } from '../types';
|
||||
|
||||
interface Props {
|
||||
blocksUntilUpdate?: BN;
|
||||
curatorId: AccountId;
|
||||
description: string;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
interface ActionProperties {
|
||||
buttonName: string;
|
||||
filter: string[];
|
||||
header: string;
|
||||
helpMessage: string;
|
||||
params: any[] | (() => any[]) | undefined;
|
||||
proposingAccountTip: string;
|
||||
tip: string;
|
||||
title: string;
|
||||
tx: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
}
|
||||
|
||||
const BOUNTY_METHODS = ['unassignCurator'];
|
||||
|
||||
function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, proposals, status }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { members } = useMembers();
|
||||
const { unassignCurator } = useBounties();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [threshold, setThreshold] = useState<BN>();
|
||||
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<Record<ValidUnassignCuratorAction, ActionProperties>>(() => ({
|
||||
GiveUp: {
|
||||
buttonName: t('Give Up'),
|
||||
filter: [curatorId.toString()],
|
||||
header: t('This action will unassign you from a curator role.'),
|
||||
helpMessage: t("The Curator account that will give up on it's role"),
|
||||
params: [index],
|
||||
proposingAccountTip: t('The account that will create the transaction.'),
|
||||
tip: t('You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.'),
|
||||
title: t("Give up curator's role"),
|
||||
tx: unassignCurator
|
||||
},
|
||||
SlashCuratorAction: {
|
||||
buttonName: t('Slash Curator'),
|
||||
filter: allAccounts,
|
||||
header: t('This action will Slash the Curator.'),
|
||||
helpMessage: t('The Curator that will be slashed.'),
|
||||
params: [index],
|
||||
proposingAccountTip: t('The account that will create the transaction.'),
|
||||
tip: t("Curator's deposit will be slashed and curator will be unassigned. Bounty will return to the Funded state."),
|
||||
title: t('Slash curator'),
|
||||
tx: unassignCurator
|
||||
},
|
||||
SlashCuratorMotion: {
|
||||
buttonName: t('Slash Curator'),
|
||||
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],
|
||||
proposingAccountTip: t('The council member that will create the motion, submission equates to an "aye" vote.'),
|
||||
tip: t("If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state."),
|
||||
title: t('Slash curator'),
|
||||
tx: api.tx.council.propose
|
||||
},
|
||||
UnassignCurator: {
|
||||
buttonName: t('Unassign Curator'),
|
||||
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],
|
||||
proposingAccountTip: t('The council member that will create the motion, submission equates to an "aye" vote.'),
|
||||
tip: t('If the motion is outvoted, 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, curatorId, index, unassignCurator, api.tx.council.propose, allAccounts, members, threshold, unassignCuratorProposal]);
|
||||
|
||||
const isVotingInitiated = useMemo(() => proposals?.filter(({ proposal }) => BOUNTY_METHODS.includes(proposal.method)).length !== 0, [proposals]);
|
||||
|
||||
const userRole = useUserRole(curatorId);
|
||||
|
||||
const action = determineUnassignCuratorAction(userRole, status, blocksUntilUpdate);
|
||||
|
||||
if (action === 'None') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { buttonName, filter, header, helpMessage, params, proposingAccountTip, tip, title, tx } = actionProperties[action];
|
||||
|
||||
return !isVotingInitiated
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='user-slash'
|
||||
label={buttonName}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={`${title} - "${truncateTitle(description, 30)}"`}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{header}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={filter}
|
||||
help={t<string>('The account that will sign the transaction.')}
|
||||
label={t<string>('proposing account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{proposingAccountTip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={curatorId}
|
||||
help={helpMessage}
|
||||
isDisabled
|
||||
label={t<string>('current curator')}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{tip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label='Approve'
|
||||
onStart={toggleOpen}
|
||||
params={params}
|
||||
tx={tx}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export default React.memo(SlashCurator);
|
||||
@ -6,13 +6,14 @@ import type { Balance, BlockNumber, BountyIndex, BountyStatus } from '@polkadot/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { getBountyStatus } from '../helpers';
|
||||
import BountyClaimAction from './BountyClaimAction';
|
||||
import BountyCuratorProposedActions from './BountyCuratorProposedActions';
|
||||
import BountyInitiateVoting from './BountyInitiateVoting';
|
||||
import CloseBounty from './CloseBounty';
|
||||
import ExtendBountyExpiryAction from './ExtendBountyExpiryAction';
|
||||
import { getBountyStatus } from './helpers';
|
||||
import ProposeCuratorAction from './ProposeCuratorAction';
|
||||
import SlashCurator from './SlashCurator';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
@ -26,8 +27,9 @@ interface Props {
|
||||
export function BountyActions ({ bestNumber, description, index, proposals, status, value }: Props): JSX.Element {
|
||||
const updateStatus = useCallback(() => getBountyStatus(status), [status]);
|
||||
|
||||
const { beneficiary, curator, unlockAt } = updateStatus();
|
||||
const { beneficiary, curator, unlockAt, updateDue } = updateStatus();
|
||||
|
||||
const blocksUntilUpdate = useMemo(() => updateDue?.sub(bestNumber), [bestNumber, updateDue]);
|
||||
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
|
||||
|
||||
return (
|
||||
@ -72,6 +74,16 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat
|
||||
payoutDue={blocksUntilPayout}
|
||||
/>
|
||||
}
|
||||
{(status.isCuratorProposed || status.isActive || status.isPendingPayout) && curator && (
|
||||
<SlashCurator
|
||||
blocksUntilUpdate={blocksUntilUpdate}
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { determineUnassignCuratorAction } from '@polkadot/app-bounties/helpers/determineUnassignCuratorAction';
|
||||
import { createAugmentedApi } from '@polkadot/test-support/api';
|
||||
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
|
||||
|
||||
describe('adjust slash curator component for', () => {
|
||||
const augmentedApi = createAugmentedApi();
|
||||
const { aBountyStatus } = new BountyFactory(augmentedApi);
|
||||
|
||||
it('Member in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('Active'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorMotion');
|
||||
});
|
||||
|
||||
it('Member in CuratorProposed state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('CuratorProposed'));
|
||||
|
||||
expect(displayAs).toEqual('UnassignCurator');
|
||||
});
|
||||
|
||||
it('Member in PendingPayout state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorMotion');
|
||||
});
|
||||
|
||||
it('Curator in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Curator', aBountyStatus('Active'));
|
||||
|
||||
expect(displayAs).toEqual('GiveUp');
|
||||
});
|
||||
|
||||
it('User in Active state with update due blocks remaining', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('Active'), new BN('1'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
});
|
||||
|
||||
it('User in Active state with no updated state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('Active'), new BN('-1'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorAction');
|
||||
});
|
||||
|
||||
it('Curator in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Curator', aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
});
|
||||
|
||||
it('User in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { UnassignCuratorAction, UserRole } from '../types';
|
||||
|
||||
export function determineUnassignCuratorAction (role: UserRole, status: BountyStatus, blocksUntilUpdate?: BN): UnassignCuratorAction {
|
||||
if (status.isCuratorProposed && role === 'Member') {
|
||||
return 'UnassignCurator';
|
||||
}
|
||||
|
||||
if (status.isActive) {
|
||||
if (role === 'Curator') {
|
||||
return 'GiveUp';
|
||||
}
|
||||
|
||||
if (role === 'Member') {
|
||||
return 'SlashCuratorMotion';
|
||||
}
|
||||
|
||||
if (role === 'User' && blocksUntilUpdate && blocksUntilUpdate.lt(BN_ZERO)) {
|
||||
return 'SlashCuratorAction';
|
||||
}
|
||||
}
|
||||
|
||||
if (status.isPendingPayout && role === 'Member') {
|
||||
return 'SlashCuratorMotion';
|
||||
}
|
||||
|
||||
return 'None';
|
||||
}
|
||||
@ -5,3 +5,4 @@ export * from './calculateBountyBond';
|
||||
export * from './getBountyStatus';
|
||||
export * from './isClaimable';
|
||||
export * from './stringHelpers';
|
||||
export * from './determineUnassignCuratorAction';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2021 @polkadot/app-council authors & contributors
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
|
||||
@ -3,3 +3,4 @@
|
||||
|
||||
export * from './useBounties';
|
||||
export * from './useBalance';
|
||||
export * from './useUserRole';
|
||||
|
||||
32
packages/page-bounties/src/hooks/useUserRole.ts
Normal file
32
packages/page-bounties/src/hooks/useUserRole.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useAccounts, useMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import { UserRole } from '../types';
|
||||
|
||||
export function useUserRole (curatorId: AccountId): UserRole {
|
||||
const { allAccounts, hasAccounts } = useAccounts();
|
||||
|
||||
const { isMember } = useMembers();
|
||||
|
||||
const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
|
||||
|
||||
if (isCurator) {
|
||||
return 'Curator';
|
||||
}
|
||||
|
||||
if (isMember) {
|
||||
return 'Member';
|
||||
}
|
||||
|
||||
if (hasAccounts) {
|
||||
return 'User';
|
||||
}
|
||||
|
||||
return 'None';
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2021 @polkadot/app-explorer authors & contributors
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { UseTranslationResponse } from 'react-i18next';
|
||||
|
||||
@ -15,3 +15,6 @@ export type HelpMessages = Record<StatusName, string>;
|
||||
|
||||
export type StatusName = 'Active' | 'Approved' | 'CuratorProposed' | 'Funded' | 'PendingPayout' | 'Proposed';
|
||||
export type BountyVotingStatuses = { [status in StatusName]: string[] };
|
||||
export type ValidUnassignCuratorAction = 'GiveUp' | 'UnassignCurator' | 'SlashCuratorMotion' | 'SlashCuratorAction';
|
||||
export type UnassignCuratorAction = ValidUnassignCuratorAction | 'None';
|
||||
export type UserRole = 'User' | 'Member' | 'Curator' | 'None';
|
||||
|
||||
@ -18,11 +18,11 @@ export const defaultBountyApi: BountyApi = {
|
||||
claimBounty: jest.fn(),
|
||||
closeBounty: jest.fn().mockReturnValue({ length: 4 }),
|
||||
dataDepositPerByte: new BN(1),
|
||||
extendBountyExpiry: jest.fn().mockReturnValue('mockProposeExtrinsic'),
|
||||
extendBountyExpiry: jest.fn().mockReturnValue('mockExtendExtrinsic'),
|
||||
maximumReasonLength: 100,
|
||||
proposeBounty: jest.fn(),
|
||||
proposeCurator: jest.fn(),
|
||||
unassignCurator: jest.fn()
|
||||
unassignCurator: jest.fn().mockReturnValue('mockUnassignExtrinsic')
|
||||
};
|
||||
|
||||
export const defaultBalance = balanceOf(1);
|
||||
@ -39,5 +39,3 @@ export const ferdie = '5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL';
|
||||
export const defaultCurator = '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM';
|
||||
|
||||
export const defaultMembers = { isMember: true, members: [alice, bob, ferdie] };
|
||||
|
||||
export const defaultAccounts = { allAccounts: [defaultCurator] };
|
||||
|
||||
@ -24,6 +24,9 @@ export class BountyFactory {
|
||||
public aBountyStatus = (status: string): BountyStatus =>
|
||||
this.#registry.createType('BountyStatus', status);
|
||||
|
||||
public bountyStatusWith = ({ curator = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', status = 'Active', updateDue = 100000 } = {}): BountyStatus =>
|
||||
this.#registry.createType('BountyStatus', { active: { curator, updateDue }, status });
|
||||
|
||||
public bountyWith = ({ status = 'Proposed', value = 1 } = {}): Bounty =>
|
||||
this.aBounty({ status: this.aBountyStatus(status), value: balanceOf(value) })
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user