Bounties - Add "Accept curator role" modal (#4566)
This commit is contained in:
parent
5487db69b4
commit
a790447add
@ -1,6 +1,7 @@
|
||||
{
|
||||
"A reward for a curator, this amount is included in the total value of the bounty.": "A reward for a curator, this amount is included in the total value of the bounty.",
|
||||
"Accept": "Accept",
|
||||
"Accept Curator Role": "Accept Curator Role",
|
||||
"Account does not have enough funds.": "Account does not have enough funds.",
|
||||
"Add Bounty": "Add Bounty",
|
||||
"After a bounty was proposed the council decides whether to fund it or not.": "After a bounty was proposed the council decides whether to fund it or not.",
|
||||
@ -26,6 +27,7 @@
|
||||
"Curator under voting": "Curator under voting",
|
||||
"Curator's account that will award a bounty to the Beneficiary.": "Curator's account that will award a bounty to the Beneficiary.",
|
||||
"Curator's deposit": "Curator's deposit",
|
||||
"Curator's deposit depends on bounty fee.": "Curator's deposit depends on bounty fee.",
|
||||
"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.",
|
||||
@ -43,6 +45,7 @@
|
||||
"Nay: {{count}}": "Nay: {{count}}",
|
||||
"No open bounties": "No open bounties",
|
||||
"Only curator can extend the bounty time.": "Only curator can extend the bounty time.",
|
||||
"Only the account proposed as curator by the council can create the assign curator transaction ": "Only the account proposed as curator by the council can create the assign curator transaction ",
|
||||
"Only the account proposed as curator by the council can create the unassign curator transaction ": "Only the account proposed as curator by the council can create the unassign curator transaction ",
|
||||
"Part of the bounty value that will go to the Curator as a reward for their work": "Part of the bounty value that will go to the Curator as a reward for their work",
|
||||
"Propose Curator": "Propose Curator",
|
||||
@ -74,9 +77,11 @@
|
||||
"The note that will be added to the transaction. It won't be stored on chain": "The note that will be added to the transaction. It won't be stored on chain",
|
||||
"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 be used to create the accept curator transaction.": "This account will be used to create the accept curator transaction.",
|
||||
"This account will be used to create the unassign curator transaction.": "This account will be used to create the unassign curator 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 accept your candidacy for the curator of the bounty.": "This action will accept your candidacy for the curator of the bounty.",
|
||||
"This action will award the Beneficiary and close the bounty after a delay.": "This action will award the Beneficiary and close the bounty after a delay.",
|
||||
"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.",
|
||||
@ -87,6 +92,8 @@
|
||||
"This action will reject your candidacy for the curator of the bounty.": "This action will reject your candidacy for the curator of the 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 amount will be reserved from your account and returned on bounty claim or if you give up, unless you are slashed earlier.": "This amount will be reserved from your account and returned on bounty claim or if you give up, unless you are slashed earlier.",
|
||||
"This amount will be sent to your account on bounty claim.": "This amount will be sent to your account on bounty claim.",
|
||||
"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.",
|
||||
@ -100,6 +107,7 @@
|
||||
"Waiting for Curator's acceptance": "Waiting for Curator's acceptance",
|
||||
"Warning": "Warning",
|
||||
"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.",
|
||||
"accept curator role": "accept curator role",
|
||||
"active": "active",
|
||||
"award bounty": "award bounty",
|
||||
"award with account": "award with account",
|
||||
@ -111,6 +119,7 @@
|
||||
"close bounty": "close bounty",
|
||||
"curator": "curator",
|
||||
"curator account": "curator account",
|
||||
"curator's deposit": "curator's deposit",
|
||||
"curator's fee": "curator's fee",
|
||||
"current curator": "current curator",
|
||||
"next action": "next action",
|
||||
|
||||
@ -34,8 +34,7 @@ import { alice,
|
||||
defaultBountyApi,
|
||||
defaultBountyUpdatePeriod,
|
||||
defaultMembers,
|
||||
defaultTreasury,
|
||||
ferdie } from '../test/hooks/defaults';
|
||||
defaultTreasury } from '../test/hooks/defaults';
|
||||
import { clickButtonWithName } from '../test/utils/clickButtonWithName';
|
||||
import { clickElementWithTestId } from '../test/utils/clickElementWithTestId';
|
||||
import { clickElementWithText } from '../test/utils/clickElementWithText';
|
||||
@ -93,7 +92,7 @@ let augmentedApi: ApiPromise;
|
||||
let queueExtrinsic: QueueTxExtrinsicAdd;
|
||||
let aBountyStatus: (status: string) => BountyStatus;
|
||||
let aBountyIndex: (index?:number) => BountyIndex;
|
||||
let aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
|
||||
let aBounty: ({ fee, status, value }?: Partial<Bounty>) => Bounty;
|
||||
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
let bountyStatusWith: ({ curator, status, updateDue }: { curator?: string, status?: string, updateDue?: number}) => BountyStatus;
|
||||
|
||||
@ -315,7 +314,7 @@ describe('Bounties', () => {
|
||||
describe('close bounty modal', () => {
|
||||
it('creates closeBounty proposal', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const { findByTestId, findByText, getByRole } = renderOneBounty(bounty);
|
||||
const { findByRole, findByTestId, findByText, getByRole } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
@ -326,12 +325,13 @@ describe('Bounties', () => {
|
||||
const combobox = getByRole('combobox');
|
||||
const proposingAccountInput = combobox.children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: ferdie } });
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
|
||||
const closeBountyButton = getByRole('button', { name: 'Close Bounty' });
|
||||
await clickButtonWithName('Close Bounty', findByRole);
|
||||
|
||||
fireEvent.click(closeBountyButton);
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: ferdie, extrinsic: 'mockProposeExtrinsic' }));
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
expect(mockBountyApi.closeBounty).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
|
||||
it('Not available when close bounty motion already exists', async () => {
|
||||
@ -371,6 +371,31 @@ describe('Bounties', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accept curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({ fee: balanceOf(20), status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' }) });
|
||||
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickButtonWithName('Accept', findByRole);
|
||||
|
||||
expect(await findByText('This action will accept your candidacy for the curator of the bounty.')).toBeTruthy();
|
||||
|
||||
const fee = (await findByTestId("curator's fee")).getAttribute('value');
|
||||
|
||||
expect(fee).toEqual('20.0000');
|
||||
|
||||
const deposit = (await findByTestId("curator's deposit")).getAttribute('value');
|
||||
|
||||
expect(deposit).toEqual('10.0000');
|
||||
|
||||
await clickButtonWithName('Accept Curator Role', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: bob }));
|
||||
expect(mockBountyApi.acceptCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('extend bounty expiry action modal', () => {
|
||||
it('queues extend bounty expiry extrinsic on submit', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
@ -122,6 +122,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
<BountyActions
|
||||
bestNumber={bestNumber}
|
||||
description={description}
|
||||
fee={fee}
|
||||
index={index}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
index: BountyIndex;
|
||||
}
|
||||
|
||||
function BountyAcceptCurator ({ curatorId, index }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { acceptCurator } = useBounties();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCurator &&
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='check'
|
||||
label={t<string>('Accept')}
|
||||
params={[index]}
|
||||
tx={acceptCurator}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BountyAcceptCurator);
|
||||
107
packages/page-bounties/src/BountyActions/BountyAcceptCurator.tsx
Normal file
107
packages/page-bounties/src/BountyActions/BountyAcceptCurator.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { permillOf } from '../helpers';
|
||||
import { useBounties, useUserRole } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
fee: BN;
|
||||
index: BountyIndex;
|
||||
}
|
||||
|
||||
function BountyAcceptCurator ({ curatorId, fee, index }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { acceptCurator } = useBounties();
|
||||
const { isCurator } = useUserRole(curatorId);
|
||||
const { bountyCuratorDeposit } = useBounties();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
|
||||
const deposit = useMemo(() => permillOf(fee, bountyCuratorDeposit), [fee, bountyCuratorDeposit]);
|
||||
|
||||
return isCurator
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='check'
|
||||
isDisabled={false}
|
||||
label={t<string>('Accept')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t<string>('accept curator role')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will accept your candidacy for the curator of the bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
help={t<string>('This account will be used to create the accept curator transaction.')}
|
||||
isDisabled
|
||||
label={t<string>('curator account')}
|
||||
type='account'
|
||||
value={curatorId.toString()}
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('Only the account proposed as curator by the council can create the assign curator transaction ')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
defaultValue={fee.toString()}
|
||||
isDisabled
|
||||
label={t<string>("curator's fee")}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This amount will be sent to your account on bounty claim.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
defaultValue={deposit.toString()}
|
||||
help={t<string>("Curator's deposit depends on bounty fee.")}
|
||||
isDisabled
|
||||
label={t<string>("curator's deposit")}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This amount will be reserved from your account and returned on bounty claim or if you give up, unless you are slashed earlier.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='check'
|
||||
label={t<string>('Accept Curator Role')}
|
||||
onStart={toggleOpen}
|
||||
params={[index]}
|
||||
tx={acceptCurator}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export default React.memo(BountyAcceptCurator);
|
||||
@ -4,11 +4,12 @@
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { Balance, BlockNumber, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import BountyAcceptCurator from '../BountyAcceptCurator';
|
||||
import { useBountyStatus } from '../hooks';
|
||||
import AwardBounty from './AwardBounty';
|
||||
import BountyAcceptCurator from './BountyAcceptCurator';
|
||||
import BountyClaimAction from './BountyClaimAction';
|
||||
import BountyInitiateVoting from './BountyInitiateVoting';
|
||||
import ProposeCuratorAction from './ProposeCuratorAction';
|
||||
@ -16,13 +17,14 @@ import ProposeCuratorAction from './ProposeCuratorAction';
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
description: string;
|
||||
fee?: BN;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
value: Balance;
|
||||
}
|
||||
|
||||
export function BountyActions ({ bestNumber, description, index, proposals, status, value }: Props): JSX.Element {
|
||||
export function BountyActions ({ bestNumber, description, fee, index, proposals, status, value }: Props): JSX.Element {
|
||||
const { beneficiary, curator, unlockAt } = useBountyStatus(status);
|
||||
|
||||
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
|
||||
@ -43,9 +45,10 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat
|
||||
value={value}
|
||||
/>
|
||||
}
|
||||
{status.isCuratorProposed && curator &&
|
||||
{status.isCuratorProposed && curator && fee &&
|
||||
<BountyAcceptCurator
|
||||
curatorId={curator}
|
||||
fee={fee}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -6,3 +6,4 @@ export * from './getBountyStatus';
|
||||
export * from './isClaimable';
|
||||
export * from './stringHelpers';
|
||||
export * from './determineUnassignCuratorAction';
|
||||
export * from './permillOf';
|
||||
|
||||
8
packages/page-bounties/src/helpers/permillOf.ts
Normal file
8
packages/page-bounties/src/helpers/permillOf.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
export function permillOf (value: BN, perMill: BN): BN {
|
||||
return value.mul(perMill).div(new BN(1_000_000));
|
||||
}
|
||||
@ -15,6 +15,7 @@ export type BountyApi = {
|
||||
awardBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
bestNumber?: BlockNumber,
|
||||
bounties?: DeriveBounties,
|
||||
bountyCuratorDeposit: BN,
|
||||
bountyDepositBase: BN,
|
||||
bountyIndex?: BN,
|
||||
bountyUpdatePeriod?: BN,
|
||||
@ -35,6 +36,7 @@ export function useBounties (): BountyApi {
|
||||
const bountyIndex = useCall<BountyIndex>((api.query.bounties || api.query.treasury).bountyCount);
|
||||
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber);
|
||||
const constsBase = api.consts.bounties || api.consts.treasury;
|
||||
const bountyCuratorDeposit = (constsBase.bountyCuratorDeposit as BalanceOf).toBn();
|
||||
const bountyDepositBase = (constsBase.bountyDepositBase as BalanceOf).toBn();
|
||||
const bountyValueMinimum = (constsBase.bountyValueMinimum as BalanceOf).toBn();
|
||||
const maximumReasonLength = constsBase.maximumReasonLength.toNumber();
|
||||
@ -56,6 +58,7 @@ export function useBounties (): BountyApi {
|
||||
awardBounty,
|
||||
bestNumber,
|
||||
bounties,
|
||||
bountyCuratorDeposit,
|
||||
bountyDepositBase,
|
||||
bountyIndex,
|
||||
bountyUpdatePeriod,
|
||||
|
||||
@ -17,6 +17,7 @@ export const defaultBountyApi: BountyApi = {
|
||||
awardBounty: jest.fn().mockReturnValue('mockAwardExtrinsic'),
|
||||
bestNumber: new BN(1) as BlockNumber,
|
||||
bounties: [] as DeriveBounties,
|
||||
bountyCuratorDeposit: balanceOf(500000),
|
||||
bountyDepositBase: new BN(1),
|
||||
bountyUpdatePeriod: defaultBountyUpdatePeriod,
|
||||
bountyValueMinimum: new BN(1),
|
||||
|
||||
@ -39,6 +39,6 @@ export class BountyFactory {
|
||||
public bountyWith = ({ status = 'Proposed', value = 1 } = {}): Bounty =>
|
||||
this.aBounty({ status: this.aBountyStatus(status), value: balanceOf(value) })
|
||||
|
||||
public aBounty = ({ status = this.aBountyStatus('Proposed'), value = balanceOf(1) }: Partial<Bounty> = {}): Bounty =>
|
||||
this.#registry.createType('Bounty', { status, value });
|
||||
public aBounty = ({ fee = balanceOf(10), status = this.aBountyStatus('Proposed'), value = balanceOf(500) }: Partial<Bounty> = {}): Bounty =>
|
||||
this.#registry.createType('Bounty', { fee, status, value });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user