Bounties - Award beneficiary action (#4501)

This commit is contained in:
MiZiet 2021-01-27 12:49:06 +01:00 committed by GitHub
parent 6839dfe7a0
commit f2b1b3f03c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 139 additions and 2 deletions

View File

@ -8,18 +8,22 @@
"Approval under voting": "Approval under voting",
"Approve": "Approve",
"Assign curator": "Assign curator",
"Award Beneficiary": "Award Beneficiary",
"Award bounty to a beneficiary account. The beneficiary will be able to claim the funds after a delay.": "Award bounty to a beneficiary account. The beneficiary will be able to claim the funds after a delay.",
"Aye": "Aye",
"Aye: {{count}}": "Aye: {{count}}",
"Beneficiary": "Beneficiary",
"Bond": "Bond",
"Bounties": "Bounties",
"Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.": "Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.",
"Choose the Beneficiary for this bounty.": "Choose the Beneficiary for this bounty.",
"Claim": "Claim",
"Claimable": "Claimable",
"Close": "Close",
"Close Bounty": "Close Bounty",
"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 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 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",
@ -49,6 +53,7 @@
"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 be used to send this transaction. Any applicable fees will be paid by this account.": "The Curator account that will be used to send this transaction. Any applicable fees will be paid by this account.",
"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",
@ -65,6 +70,7 @@
"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 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.",
"This action will create a Council motion to slash the Curator.": "This action will create a Council motion to slash the Curator.",
@ -83,6 +89,9 @@
"Unassign curator under voting": "Unassign curator under voting",
"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",
"award bounty": "award bounty",
"award with account": "award with account",
"beneficiary account": "beneficiary account",
"bounty bond": "bounty bond",
"bounty remark": "bounty remark",
"bounty requested allocation": "bounty requested allocation",

View File

@ -22,7 +22,7 @@ import { QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Stat
import { createAugmentedApi } from '@polkadot/test-support/api';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
import { aliceSigner, MemoryStore } from '@polkadot/test-support/keyring';
import { MemoryStore } from '@polkadot/test-support/keyring';
import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';
import { extractTime } from '@polkadot/util';
@ -287,7 +287,6 @@ describe('Bounties', () => {
const proposingAccountInput = comboboxes[0].children[0];
const proposingCuratorInput = comboboxes[1].children[0];
const alice = aliceSigner().address;
fireEvent.change(proposingAccountInput, { target: { value: alice } });
fireEvent.change(proposingCuratorInput, { target: { value: alice } });
@ -376,6 +375,7 @@ describe('Bounties', () => {
fireEvent.click(acceptButton);
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockUnassignExtrinsic' }));
expect(mockBountyApi.unassignCurator).toHaveBeenCalledWith(aBountyIndex(0));
});
it('for bounty in PendingPayout state', async () => {
@ -402,6 +402,31 @@ describe('Bounties', () => {
});
});
describe('award beneficiary action modal', () => {
it('awards the beneficiary ', async () => {
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
const { findByText, getAllByRole } = renderOneBounty(bounty);
const awardBeneficiaryButton = await findByText('Award Beneficiary');
fireEvent.click(awardBeneficiaryButton);
expect(await findByText('This action will award the Beneficiary and close the bounty after a delay.')).toBeTruthy();
const comboboxes = getAllByRole('combobox');
const beneficiaryAccountInput = comboboxes[1].children[0];
fireEvent.change(beneficiaryAccountInput, { target: { value: bob } });
fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
const acceptButton = await findByText('Approve');
fireEvent.click(acceptButton);
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockAwardExtrinsic' }));
expect(mockBountyApi.awardBounty).toHaveBeenCalledWith(aBountyIndex(0), bob);
});
});
describe('status is extended', () => {
it('on proposed curator', async () => {
const bounty = bountyWith({ status: 'Funded' });

View File

@ -0,0 +1,92 @@
// Copyright 2017-2021 @polkadot/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
import React, { useMemo, useState } from 'react';
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useAccounts, useToggle } from '@polkadot/react-hooks';
import { useBounties } from '../hooks';
import { useTranslation } from '../translate';
interface Props {
curatorId: AccountId
index: BountyIndex;
}
function AwardBounty ({ curatorId, index }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { awardBounty } = useBounties();
const { allAccounts } = useAccounts();
const [isOpen, toggleOpen] = useToggle();
const [beneficiaryId, setBeneficiaryId] = useState<string | null>(null);
const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
return isCurator
? (
<>
<Button
icon='award'
isDisabled={false}
label={t<string>('Award Beneficiary')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
header={t<string>('award bounty')}
size='large'
>
<Modal.Content>
<Modal.Column>
<p>{t<string>('This action will award the Beneficiary and close the bounty after a delay.')}</p>
</Modal.Column>
<Modal.Columns>
<Modal.Column>
<InputAddress
defaultValue={curatorId}
help={t<string>("Curator's account that will award a bounty to the Beneficiary.")}
isDisabled={true}
label={t<string>('award with account')}
type='account'
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The Curator account that will be used to send this transaction. Any applicable fees will be paid by this account.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddress
help={t<string>('Choose the Beneficiary for this bounty.')}
label={t<string>('beneficiary account')}
onChange={setBeneficiaryId}
type='account'
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Award bounty to a beneficiary account. The beneficiary will be able to claim the funds after a delay.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton
accountId={curatorId}
icon='check'
label={t<string>('Approve')}
onStart={toggleOpen}
params={[index, beneficiaryId]}
tx={awardBounty}
/>
</Modal.Actions>
</Modal>
)}
</>
)
: null;
}
export default React.memo(AwardBounty);

View File

@ -7,6 +7,7 @@ import type { Balance, BlockNumber, BountyIndex, BountyStatus } from '@polkadot/
import React, { useCallback, useMemo } from 'react';
import { getBountyStatus } from '../helpers';
import AwardBounty from './AwardBounty';
import BountyClaimAction from './BountyClaimAction';
import BountyCuratorProposedActions from './BountyCuratorProposedActions';
import BountyInitiateVoting from './BountyInitiateVoting';
@ -84,6 +85,12 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat
status={status}
/>
)}
{status.isActive && curator &&
<AwardBounty
curatorId={curator}
index={index}
/>
}
</>
);
}

View File

@ -12,6 +12,7 @@ import { BalanceOf, BlockNumber, BountyIndex } from '@polkadot/types/interfaces'
export type BountyApi = {
acceptCurator: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
approveBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
awardBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
bestNumber?: BlockNumber,
bounties?: DeriveBounties,
bountyDepositBase: BN,
@ -45,10 +46,12 @@ export function useBounties (): BountyApi {
const closeBounty = (api.tx.bounties || api.tx.treasury).closeBounty;
const extendBountyExpiry = (api.tx.bounties || api.tx.treasury).extendBountyExpiry;
const unassignCurator = (api.tx.bounties || api.tx.treasury).unassignCurator;
const awardBounty = (api.tx.bounties || api.tx.treasury).awardBounty;
return {
acceptCurator,
approveBounty,
awardBounty,
bestNumber,
bounties,
bountyDepositBase,

View File

@ -11,6 +11,7 @@ import { BlockNumber } from '@polkadot/types/interfaces';
export const defaultBountyApi: BountyApi = {
acceptCurator: jest.fn(),
approveBounty: jest.fn(),
awardBounty: jest.fn().mockReturnValue('mockAwardExtrinsic'),
bestNumber: new BN(1) as BlockNumber,
bounties: [] as DeriveBounties,
bountyDepositBase: new BN(1),