Skip to content

Commit

Permalink
Add propose curator action to Bounties (polkadot-js#4417)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiZiet authored Jan 15, 2021
1 parent 566d7d4 commit 9084d42
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 29 deletions.
23 changes: 18 additions & 5 deletions packages/apps/public/locales/en/app-bounties.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
{
"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",
"Account does not have enough funds.": "Account does not have enough funds.",
"Add Bounty": "Add Bounty",
"Allocation value is smaller than the minimum bounty value.": "Allocation value is smaller than the minimum bounty value.",
"Approval under voting": "Approval under voting",
"Approve": "Approve",
"Assign curator": "Assign curator",
"Bond": "Bond",
"Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.": "Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.",
"Bounties": "Bounties",
"Calculated bond for bounty": "Calculated bond for bounty",
"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.",
"Claim": "Claim",
"Claimable": "Claimable",
"Curator under voting": "Curator under voting",
"Curators deposit": "Curators deposit",
"Curators fee": "Curators fee",
"Curator's deposit": "Curator's deposit",
"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.",
"Curator's fee.": "Curator's fee.",
"Description of the Bounty (to be stored on-chain)": "Description of the Bounty (to be stored on-chain)",
"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.",
"Initiate Voting": "Initiate Voting",
"No open bounties": "No open bounties",
"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",
"Proposer": "Proposer",
"Proposer bond depends on bounty title length.": "Proposer bond depends on bounty title length.",
"Reject": "Reject",
"Rejection under voting": "Rejection under voting",
"Select Curator.": "Select Curator.",
"Select an account which (after a successful vote) will act as a curator.": "Select an account which (after a successful vote) will act as a curator.",
"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.",
"The council member that is will create a motion, submission equates to an \"aye\" vote for chosen option.": "The council member that is 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 a motion, submission equates to an \"aye\" vote for chosen option.",
"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 total payment amount of this bounty, curators fee included.": "The total payment amount of this bounty, curators fee included.",
"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 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 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 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.",
"Title too long": "Title too long",
Expand All @@ -42,10 +51,14 @@
"bounty requested allocation": "bounty requested allocation",
"bounty title": "bounty title",
"curator": "curator",
"curator account": "curator account",
"curator's fee": "curator's fee",
"next bounty funding in": "next bounty funding in",
"next burn": "next burn",
"past": "past",
"payout due": "payout due",
"proposing account": "proposing account",
"select curator": "select curator",
"spend period": "spend period",
"submit with account": "submit with account",
"title": "title",
Expand Down
4 changes: 2 additions & 2 deletions packages/apps/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"Controller account": "",
"Create a backup file for this account": "",
"Curator under voting": "",
"Curators deposit": "",
"Curators fee": "",
"Curator's deposit": "",
"Curator's fee": "",
"Dark theme (experimental, work-in-progress)": "",
"Delegate democracy votes": "",
"Deploy": "",
Expand Down
4 changes: 2 additions & 2 deletions packages/page-bounties/src/Bounties.slow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ describe('--SLOW--: Bounties', () => {
it('list shows an existing bounty', async () => {
const api = await createApi();

await execute(api.tx.bounties.proposeBounty(new BN(500_000_000_000_000), 'new bounty hello hello more bytes'), aliceSigner());
await execute(api.tx.bounties.proposeBounty(new BN(500_000_000_000_000), 'a short bounty title'), aliceSigner());

const { findByText } = renderBounties();

expect(await findByText('new bounty hello hello more bytes', {}, { timeout: 20_000 })).toBeTruthy();
expect(await findByText('a short bounty title', {}, { timeout: 20_000 })).toBeTruthy();
});
});
131 changes: 119 additions & 12 deletions packages/page-bounties/src/Bounties.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { DeriveBounties, DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BlockNumber, Bounty, BountyIndex } from '@polkadot/types/interfaces';
import type { BlockNumber, Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';

import { fireEvent, render } from '@testing-library/react';
import BN from 'bn.js';
Expand All @@ -19,13 +19,21 @@ import metaStatic from '@polkadot/metadata/static';
import { ApiContext } from '@polkadot/react-api';
import { ApiProps } from '@polkadot/react-api/types';
import i18next from '@polkadot/react-components/i18n';
import { QueueProvider } from '@polkadot/react-components/Status/Context';
import { QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
import { aliceSigner, MemoryStore } from '@polkadot/test-support/keyring';
import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';

import Bounties from './Bounties';
import { BountyApi } from './hooks';

function aBounty (): Bounty {
return new TypeRegistry().createType('Bounty');
function bountyStatus (status: string): BountyStatus {
return new TypeRegistry().createType('BountyStatus', status);
}

function aBounty ({ value = balanceOf(1), status = bountyStatus('Proposed') }: Partial<Bounty> = {}): Bounty {
return new TypeRegistry().createType('Bounty', { status, value });
}

function anIndex (index = 0): BountyIndex {
Expand All @@ -49,11 +57,13 @@ let mockBountyApi: BountyApi = {
closeBounty: jest.fn(),
dataDepositPerByte: new BN(1),
maximumReasonLength: 100,
proposeBounty: jest.fn()
proposeBounty: jest.fn(),
proposeCurator: jest.fn()
};

let mockBalance = balanceOf(1);
let apiWithAugmentations: ApiPromise;
const mockMembers = { isMember: true };

const mockTreasury = {
burn: new BN(1),
Expand Down Expand Up @@ -105,11 +115,24 @@ function aProposal (extrinsic: SubmittableExtrinsic<'promise'>) {
};
}

jest.mock('@polkadot/react-hooks/useMembers', () => {
return {
useMembers: () => mockMembers
};
});

const propose = jest.fn().mockReturnValue('mockProposeExtrinsic');
let queueExtrinsic: QueueTxExtrinsicAdd;

describe('Bounties', () => {
beforeAll(async () => {
await i18next.changeLanguage('en');
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
apiWithAugmentations = createApiWithAugmentations();
});
beforeEach(() => {
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
});

const renderBounties = (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}) => {
mockBountyApi = { ...mockBountyApi, ...bountyApi };
Expand All @@ -120,19 +143,30 @@ describe('Bounties', () => {
},
genesisHash: aGenesisHash(),
query: {},
registry: { chainDecimals: 12 }
registry: { chainDecimals: 12 },
tx: {
council: {
propose
}
}
},
systemName: 'substrate' } as unknown as ApiProps;

const queue = {
queueExtrinsic
} as QueueProps;

return render(
<Suspense fallback='...'>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<Bounties/>
</ApiContext.Provider>
</ThemeProvider>
</MemoryRouter>
<QueueProvider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<Bounties/>
</ApiContext.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueProvider>
</Suspense>
);
};
Expand Down Expand Up @@ -222,6 +256,79 @@ describe('Bounties', () => {
});
});

describe('propose curator modal', () => {
it('shows an error if fee is greater than bounty value', async () => {
const { findByTestId, findByText } = renderBounties({ bounties: [
{ bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }),
description: 'kusama comic book',
index: anIndex(),
proposals: [] }
] });
const proposeCuratorButton = await findByText('Propose Curator');

fireEvent.click(proposeCuratorButton);
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();

const feeInput = await findByTestId("curator's fee");

fireEvent.change(feeInput, { target: { value: '6' } });

expect(await findByText("Curator's fee can't be higher than bounty value.")).toBeTruthy();
});

it('disables Assign Curator button if validation fails', async () => {
const { findByTestId, findByText } = renderBounties({ bounties: [
{ bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }),
description: 'kusama comic book',
index: anIndex(),
proposals: [] }
] });
const proposeCuratorButton = await findByText('Propose Curator');

fireEvent.click(proposeCuratorButton);
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();

const feeInput = await findByTestId("curator's fee");

fireEvent.change(feeInput, { target: { value: '6' } });

const assignCuratorButton = await findByText('Assign curator');

expect(assignCuratorButton.classList.contains('isDisabled')).toBeTruthy();
});

it('queues propose extrinsic on submit', async () => {
const { findByTestId, findByText, getAllByRole } = renderBounties({ bounties: [
{ bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }),
description: 'kusama comic book',
index: anIndex(),
proposals: [] }
] });
const proposeCuratorButton = await findByText('Propose Curator');

fireEvent.click(proposeCuratorButton);
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();

const feeInput = await findByTestId("curator's fee");

fireEvent.change(feeInput, { target: { value: '0' } });

const comboboxes = getAllByRole('combobox');

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 } });

const assignCuratorButton = await findByText('Assign curator');

fireEvent.click(assignCuratorButton);
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
});
});

describe('status is extended when voting', () => {
it('on proposed curator', async () => {
const bounty = bountyInStatus('Funded');
Expand Down
10 changes: 6 additions & 4 deletions packages/page-bounties/src/Bounty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';

import { BountyActions } from './BountyActions';
import { getBountyStatus } from './helpers';
import { getBountyStatus, truncateTitle } from './helpers';
import { useTranslation } from './translate';
import VotingDescription from './VotingDescription';

Expand Down Expand Up @@ -66,7 +66,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
)}
</div>
</td>
<td>{description}</td>
<td>{truncateTitle(description, 30)}</td>
<td><FormatBalance value={value} /></td>
<td>{curator ? <AddressSmall value={curator} /> : EMPTY_CELL}</td>
<td><DueBlocks dueBlocks={blocksUntilUpdate} /></td>
Expand All @@ -75,9 +75,11 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<td>
<BountyActions
bestNumber={bestNumber}
description={description}
index={index}
proposals={proposals}
status={status}
value={value}
/>
</td>
<td className='table-column-icon'>
Expand Down Expand Up @@ -115,8 +117,8 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<div className='inline-balance'><FormatBalance value={bond} /></div>
</td>
<td className='column-with-label'>
<div className='label'>{t('Curators fee')}</div>
<div className='label'>{t('Curators deposit')}</div>
<div className='label'>{t("Curator's fee")}</div>
<div className='label'>{t("Curator's deposit")}</div>
</td>
<td>
<div className='inline-balance'>{curator ? <FormatBalance value={fee} /> : EMPTY_CELL}</div>
Expand Down
15 changes: 13 additions & 2 deletions packages/page-bounties/src/BountyActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@
// SPDX-License-Identifier: Apache-2.0

import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BlockNumber, BountyStatus } from '@polkadot/types/interfaces';
import type { Balance, BlockNumber, BountyStatus } from '@polkadot/types/interfaces';

import React, { useCallback, useMemo } from 'react';

import BountyClaimAction from './BountyClaimAction';
import BountyCuratorProposedActions from './BountyCuratorProposedActions';
import BountyInitiateVoting from './BountyInitiateVoting';
import { getBountyStatus } from './helpers';
import ProposeCuratorAction from './ProposeCuratorAction';

interface Props {
bestNumber: BlockNumber;
description: string;
index: number;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
value: Balance;
}

export function BountyActions ({ bestNumber, index, proposals, status }: Props): JSX.Element {
export function BountyActions ({ bestNumber, description, index, proposals, status, value }: Props): JSX.Element {
const updateStatus = useCallback(() => getBountyStatus(status), [status]);

const { beneficiary, curator, unlockAt } = updateStatus();
Expand All @@ -32,6 +35,14 @@ export function BountyActions ({ bestNumber, index, proposals, status }: Props):
index={index}
proposals={proposals}
/>}
{status.isFunded && (
<ProposeCuratorAction
description={description}
index={index}
proposals={proposals}
value={value}
/>
)}
{status.isCuratorProposed && curator &&
<BountyCuratorProposedActions
curatorId={curator}
Expand Down
2 changes: 1 addition & 1 deletion packages/page-bounties/src/BountyCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function BountyCreate () {
<Modal.Columns>
<Modal.Column>
<InputBalance
help={t<string>('The total payment amount of this bounty, curators fee included.')}
help={t<string>("The total payment amount of this bounty, curator's fee included.")}
isError={!isValueValid}
isZeroable
label={t<string>('bounty requested allocation')}
Expand Down
Loading

0 comments on commit 9084d42

Please sign in to comment.