diff --git a/packages/api-derive/src/bounties/bounties.spec.ts b/packages/api-derive/src/bounties/bounties.spec.ts index 0d80ed2194..0ab3144706 100644 --- a/packages/api-derive/src/bounties/bounties.spec.ts +++ b/packages/api-derive/src/bounties/bounties.spec.ts @@ -1,16 +1,20 @@ // Copyright 2017-2021 @polkadot/api-derive authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ApiInterfaceRx } from '@polkadot/api/types'; +import type { ApiInterfaceRx, SubmittableExtrinsic } from '@polkadot/api/types'; import type { Bytes, Option, StorageKey } from '@polkadot/types'; -import type { Bounty, BountyIndex } from '@polkadot/types/interfaces'; +import type { Bounty, BountyIndex, ProposalIndex } from '@polkadot/types/interfaces'; import type { Codec, InterfaceTypes } from '@polkadot/types/types'; +import { ApiPromise } from '@polkadot/api'; +import { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; +import { Proposal } from '@polkadot/types/interfaces'; import { of } from '@polkadot/x-rxjs'; import { BountyFactory } from '../../test/bountyFactory'; import { BytesFactory } from '../../test/bytesFactory'; import { createApiWithAugmentations } from '../../test/helpers'; +import { ProposalFactory } from '../../test/proposalFactory'; import { bounties } from '.'; const DEFAULT_PROPOSER = '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'; @@ -21,22 +25,62 @@ describe('bounties derive', () => { let emptyOption: (typeName: keyof InterfaceTypes) => Option; let optionOf: (value: T)=> Option; let bountyIndex: (index: number) => BountyIndex; + let proposalIndex: (index: number) => ProposalIndex; let bytes: (value: string) => Bytes; + let api: ApiPromise; + let createProposal: (method: SubmittableExtrinsic<'promise'>) => Proposal; + let defaultMockApi: ApiInterfaceRx; beforeAll(() => { - const api = createApiWithAugmentations(); + api = createApiWithAugmentations(); ({ bountyIndex, defaultBounty, emptyOption, optionOf, storageKey } = new BountyFactory(api)); ({ bytes } = new BytesFactory(api.registry)); + ({ createProposal, proposalIndex } = new ProposalFactory(api)); + }); + + beforeEach(() => { + defaultMockApi = { + derive: { + council: { + proposals: () => of([] as DeriveCollectiveProposal[]) + } + }, + query: { + council: { + proposalCount: () => of(proposalIndex(2)) + }, + treasury: { + bounties: { + keys: () => of([storageKey(0), storageKey(1)]), + multi: () => of([optionOf(defaultBounty()), optionOf(defaultBounty())]) + }, + bountyCount: () => of(bountyIndex(2)), + bountyDescriptions: { + multi: () => of([ + optionOf(bytes('make polkadot even better')), + optionOf(bytes('this will be totally ignored')) + ]) + } + } + }, + tx: api.tx + } as unknown as ApiInterfaceRx; }); it('creates storage key', function () { expect(storageKey(194).args[0].eq(194)).toBe(true); }); + it('creates proposal', function () { + expect(createProposal(api.tx.balances.transfer('5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))).toBeTruthy(); + }); + it('combines bounties with descriptions', async () => { const mockApi = { + ...defaultMockApi, query: { + ...defaultMockApi.query, treasury: { bounties: { keys: () => of([storageKey(0), storageKey(2), storageKey(3)]), @@ -64,4 +108,32 @@ describe('bounties derive', () => { expect(result[1].description).toEqual(''); expect(result[1].index.eq(3)).toBe(true); }); + + it('returns motions', async () => { + const result = await bounties('', defaultMockApi)().toPromise(); + + expect(result).toHaveLength(2); + expect(result[0].proposals).toHaveLength(0); + expect(result[1].proposals).toHaveLength(0); + }); + + it('combines bounties with motions', async () => { + const mockApi = { + ...defaultMockApi, + derive: { + council: { + proposals: () => of([ + { proposal: createProposal(api.tx.bounties.approveBounty(1)) }, + { proposal: createProposal(api.tx.treasury.approveProposal(1)) }] as DeriveCollectiveProposal[]) + } + } + } as unknown as ApiInterfaceRx; + + const result = await bounties('', mockApi)().toPromise(); + + expect(result).toHaveLength(2); + expect(result[0].proposals).toHaveLength(0); + expect(result[1].proposals).toHaveLength(1); + expect(result[1].proposals[0].proposal.method).toEqual('approveBounty'); + }); }); diff --git a/packages/api-derive/src/bounties/bounties.ts b/packages/api-derive/src/bounties/bounties.ts index c804275dbb..8ccedc6d12 100644 --- a/packages/api-derive/src/bounties/bounties.ts +++ b/packages/api-derive/src/bounties/bounties.ts @@ -2,17 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import type { ApiInterfaceRx } from '@polkadot/api/types'; -import type { DeriveBounties } from '@polkadot/api-derive/types'; +import type { DeriveBounties, DeriveCollectiveProposal } from '@polkadot/api-derive/types'; import type { Bytes, Option } from '@polkadot/types'; -import type { Bounty, BountyIndex } from '@polkadot/types/interfaces'; +import type { Bounty, BountyIndex, ProposalIndex } from '@polkadot/types/interfaces'; import { memo } from '@polkadot/api-derive/util'; import { combineLatest, Observable, of } from '@polkadot/x-rxjs'; import { map, switchMap } from '@polkadot/x-rxjs/operators'; -type Result = [Option[], Option[], BountyIndex[]]; +import { filterBountiesProposals } from './helpers/filterBountyProposals'; -function parseResult (api: ApiInterfaceRx, [maybeBounties, maybeDescriptions, ids]: Result): DeriveBounties { +type Result = [Option[], Option[], BountyIndex[], DeriveCollectiveProposal[]]; + +function parseResult ([maybeBounties, maybeDescriptions, ids, bountyProposals]: Result): DeriveBounties { const bounties: DeriveBounties = []; maybeBounties.forEach((bounty, index) => { @@ -20,7 +22,8 @@ function parseResult (api: ApiInterfaceRx, [maybeBounties, maybeDescriptions, id bounties.push({ bounty: bounty.unwrap(), description: maybeDescriptions[index].unwrapOrDefault().toUtf8(), - index: ids[index] + index: ids[index], + proposals: bountyProposals.filter((bountyProposal) => ids[index].eq(bountyProposal.proposal.args[0])) }); } }); @@ -29,23 +32,30 @@ function parseResult (api: ApiInterfaceRx, [maybeBounties, maybeDescriptions, id } export function bounties (instanceId: string, api: ApiInterfaceRx): () => Observable { - const bountyBase = api.query.bounties - ? api.query.bounties - : api.query.treasury; + const bountyBase = api.query.bounties ? api.query.bounties : api.query.treasury; return memo(instanceId, (): Observable => - bountyBase.bountyCount().pipe( - switchMap(() => bountyBase.bounties.keys()), - switchMap((keys): Observable => { + combineLatest([ + bountyBase.bountyCount(), + api.query.council.proposalCount() + ]).pipe( + switchMap(() => + combineLatest([ + bountyBase.bounties.keys(), + api.derive.council.proposals() + ]) + ), + switchMap(([keys, proposals]): Observable => { const ids = keys.map(({ args: [id] }) => id as BountyIndex); return combineLatest([ bountyBase.bounties.multi>(ids), bountyBase.bountyDescriptions.multi>(ids), - of(ids) + of(ids), + of(filterBountiesProposals(api, proposals)) ]); }), - map(([maybeBounties, maybeDescriptions, ids]) => parseResult(api, [maybeBounties, maybeDescriptions, ids])) + map(parseResult) ) ); } diff --git a/packages/api-derive/src/bounties/helpers/filterBountyProposals.ts b/packages/api-derive/src/bounties/helpers/filterBountyProposals.ts new file mode 100644 index 0000000000..375b53a4cf --- /dev/null +++ b/packages/api-derive/src/bounties/helpers/filterBountyProposals.ts @@ -0,0 +1,12 @@ +// Copyright 2017-2021 @polkadot/api-derive authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ApiInterfaceRx } from '@polkadot/api/types'; +import { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; + +export function filterBountiesProposals (api: ApiInterfaceRx, allProposals: DeriveCollectiveProposal[]): DeriveCollectiveProposal[] { + const bountyTxBase = api.tx.bounties ? api.tx.bounties : api.tx.treasury; + const bountyProposalCalls = [bountyTxBase.approveBounty, bountyTxBase.closeBounty, bountyTxBase.proposeCurator, bountyTxBase.unassignCurator]; + + return allProposals.filter((proposal) => bountyProposalCalls.find((bountyCall) => bountyCall.is(proposal.proposal))); +} diff --git a/packages/api-derive/src/types.ts b/packages/api-derive/src/types.ts index c32a1e2e10..e25b5ef17d 100644 --- a/packages/api-derive/src/types.ts +++ b/packages/api-derive/src/types.ts @@ -132,6 +132,7 @@ export interface DeriveBounty { bounty: Bounty; description: string; index: BountyIndex; + proposals: DeriveCollectiveProposal[]; } export type DeriveBounties = DeriveBounty[]; diff --git a/packages/api-derive/test/helpers.ts b/packages/api-derive/test/helpers.ts index f23390e373..25486abd16 100644 --- a/packages/api-derive/test/helpers.ts +++ b/packages/api-derive/test/helpers.ts @@ -10,7 +10,9 @@ export function createApiWithAugmentations (): ApiPromise { const registry = new TypeRegistry(); const metadata = new Metadata(registry, metaStatic); - const api = new ApiPromise({ provider: new WsProvider('ws://', false) }); + registry.setMetadata(metadata); + + const api = new ApiPromise({ provider: new WsProvider('ws://', false), registry }); api.injectMetadata(metadata, true); diff --git a/packages/api-derive/test/proposalFactory.ts b/packages/api-derive/test/proposalFactory.ts new file mode 100644 index 0000000000..4dd4a8fa58 --- /dev/null +++ b/packages/api-derive/test/proposalFactory.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2021 @polkadot/api authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ApiPromise } from '@polkadot/api'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { Proposal, ProposalIndex } from '@polkadot/types/interfaces'; +import { Registry } from '@polkadot/types/types'; + +export class ProposalFactory { + readonly #api: ApiPromise; + readonly #registry: Registry; + + constructor (api: ApiPromise) { + this.#api = api; + this.#registry = this.#api.registry; + } + + public createProposal = (method: SubmittableExtrinsic<'promise'>): Proposal => { + return this.#registry.createType('Proposal', method); + }; + + public proposalIndex = (index: number): ProposalIndex => { + return this.#registry.createType('ProposalIndex', index); + } +}