diff --git a/packages/test-support/package.json b/packages/test-support/package.json index 0f926e1156..63c6431edd 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -10,5 +10,9 @@ "dependencies": { "@testing-library/react": "^11.2.3", "testcontainers": "^6.4.1" + }, + "devDependencies": { + "ts-node": "^9.1.1", + "tsconfig-paths": "^3.9.0" } } diff --git a/packages/test-support/scripts/createBounties.ts b/packages/test-support/scripts/createBounties.ts new file mode 100644 index 0000000000..811fd33406 --- /dev/null +++ b/packages/test-support/scripts/createBounties.ts @@ -0,0 +1,48 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { createApi } from '@polkadot/test-support/api'; +import { aliceSigner, + bobSigner, + charlieSigner, + daveSigner, + eveSigner, + ferdieSigner } from '@polkadot/test-support/keyring'; + +import { multiAcceptCurator, + multiApproveBounty, + multiAwardBounty, + multiClaimBounty, + multiProposeBounty, + multiProposeCurator, + multiWaitForBountyFunded, + multiWaitForClaim } from './lib/multiFunctions'; + +(async () => { + const api = await createApi(9944); + + const SIGNERS = [aliceSigner(), bobSigner(), charlieSigner(), daveSigner(), eveSigner(), ferdieSigner()]; + + const indexes = await multiProposeBounty(api, 6); + + indexes.pop(); + await multiApproveBounty(api, indexes); + + await multiWaitForBountyFunded(api, indexes); + + indexes.pop(); + await multiProposeCurator(api, indexes, SIGNERS); + + indexes.pop(); + await multiAcceptCurator(api, indexes, SIGNERS); + + indexes.pop(); + await multiAwardBounty(api, indexes, SIGNERS); + + await multiWaitForClaim(api, indexes); + + indexes.pop(); + await multiClaimBounty(api, indexes, SIGNERS); + + await api.disconnect(); +})().catch((err) => console.error(err)); diff --git a/packages/test-support/scripts/lib/bountyWaitFunctions.ts b/packages/test-support/scripts/lib/bountyWaitFunctions.ts new file mode 100644 index 0000000000..ce9e2e71c2 --- /dev/null +++ b/packages/test-support/scripts/lib/bountyWaitFunctions.ts @@ -0,0 +1,34 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ApiPromise } from '@polkadot/api'; +import { DeriveBounty } from '@polkadot/api-derive/types'; +import { waitFor, WaitOptions } from '@polkadot/test-support/utils/waitFor'; + +type bStatus = 'isFunded' | 'isActive'; + +async function getBounty (api: ApiPromise, bountyIndex: number): Promise { + const bounties = await api.derive.bounties.bounties(); + + return bounties.find((bounty) => (bounty.index.toNumber() === bountyIndex)) as DeriveBounty; +} + +export async function waitForBountyState (api: ApiPromise, expectedState: bStatus, index: number, { interval = 500, + timeout = 10000 } = {}): Promise { + return waitFor(async () => { + const bounty = await getBounty(api, index); + + return bounty.bounty.status[expectedState]; + }, { interval, timeout }); +} + +export async function waitForClaim (api: ApiPromise, index: number, { interval = 500, timeout = 10000 }: WaitOptions): Promise { + return waitFor(async () => { + const bounty = await getBounty(api, index); + const unlockAt = bounty.bounty.status.asPendingPayout.unlockAt; + + const bestNumber = await api.derive.chain.bestNumber(); + + return unlockAt.lt(bestNumber); + }, { interval, timeout }); +} diff --git a/packages/test-support/scripts/lib/changeBountyStateFunctions.ts b/packages/test-support/scripts/lib/changeBountyStateFunctions.ts new file mode 100644 index 0000000000..9672993552 --- /dev/null +++ b/packages/test-support/scripts/lib/changeBountyStateFunctions.ts @@ -0,0 +1,47 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { KeyringPair } from '@polkadot/keyring/types'; + +import BN from 'bn.js'; + +import { ApiPromise } from '@polkadot/api'; +import { execute } from '@polkadot/test-support/transaction'; + +import { acceptMotion, fillTreasury, getMotion, proposeMotion } from './helpers'; + +export async function acceptCurator (api: ApiPromise, id: number, signer: KeyringPair): Promise { + await execute(api.tx.bounties.acceptCurator(id), signer); +} + +export async function awardBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise { + await execute(api.tx.bounties.awardBounty(index, signer.address), signer); +} + +export async function claimBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise { + await execute(api.tx.bounties.claimBounty(index), signer); +} + +export async function proposeBounty (api: ApiPromise, value: BN, title: string, signer: KeyringPair): Promise { + await execute(api.tx.bounties.proposeBounty(value, title), signer); + const index = await api.query.bounties.bountyCount(); + + return index.toNumber() - 1; +} + +export async function proposeCurator (api: ApiPromise, index: number, signer: KeyringPair): Promise { + await proposeMotion(api, api.tx.bounties.proposeCurator(index, signer.address, 10), signer); + + const bountyProposal = await getMotion(api, index); + + await acceptMotion(api, bountyProposal.hash, bountyProposal.votes?.index.toNumber() ?? 0); +} + +export async function approveBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise { + await proposeMotion(api, api.tx.bounties.approveBounty(index), signer); + + const bountyProposal = await getMotion(api, index); + + await acceptMotion(api, bountyProposal.hash, bountyProposal.votes?.index.toNumber() ?? 0); + await fillTreasury(api, signer); +} diff --git a/packages/test-support/scripts/lib/constants.ts b/packages/test-support/scripts/lib/constants.ts new file mode 100644 index 0000000000..b6ed454ec0 --- /dev/null +++ b/packages/test-support/scripts/lib/constants.ts @@ -0,0 +1,10 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; + +export const TREASURY_ADDRESS = '13UVJyLnbVp9RBZYFwFGyDvVd1y27Tt8tkntv6Q7JVPhFsTB'; +export const FUNDING_TIME = 150000; +export const PAYOUT_TIME = 150000; +export const WEIGHT_BOUND = new BN('10000000000'); +export const LENGTH_BOUND = 100000; diff --git a/packages/test-support/scripts/lib/helpers.ts b/packages/test-support/scripts/lib/helpers.ts new file mode 100644 index 0000000000..29e1ea06cc --- /dev/null +++ b/packages/test-support/scripts/lib/helpers.ts @@ -0,0 +1,42 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; + +import { ApiPromise } from '@polkadot/api'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { charlieSigner, + daveSigner, + eveSigner, + ferdieSigner } from '@polkadot/test-support/keyring'; +import { execute } from '@polkadot/test-support/transaction'; +import { Hash } from '@polkadot/types/interfaces'; + +import { LENGTH_BOUND, TREASURY_ADDRESS, WEIGHT_BOUND } from './constants'; + +export async function acceptMotion (api: ApiPromise, hash: Hash, index: number): Promise { + const charlieVote = execute(api.tx.council.vote(hash, index, true), charlieSigner()); + const daveVote = execute(api.tx.council.vote(hash, index, true), daveSigner()); + const eveVote = execute(api.tx.council.vote(hash, index, true), eveSigner()); + const ferdieVote = execute(api.tx.council.vote(hash, index, true), ferdieSigner()); + + await Promise.all([charlieVote, daveVote, eveVote, ferdieVote]); + await execute(api.tx.council.close(hash, index, WEIGHT_BOUND, LENGTH_BOUND), charlieSigner()); +} + +export async function fillTreasury (api: ApiPromise, signer: KeyringPair): Promise { + await execute(api.tx.balances.transfer(TREASURY_ADDRESS, new BN(5_000_000_000_000_000)), signer); +} + +export async function proposeMotion (api: ApiPromise, submittableExtrinsic: SubmittableExtrinsic<'promise'>, signer: KeyringPair): Promise { + await execute(api.tx.council.propose(4, submittableExtrinsic, LENGTH_BOUND), signer); +} + +export async function getMotion (api: ApiPromise, index: number): Promise { + const bounties = await api.derive.bounties.bounties(); + const bountyProposals = bounties.find((bounty) => (bounty.index.toNumber() === index))?.proposals as DeriveCollectiveProposal[]; + + return bountyProposals[0]; +} diff --git a/packages/test-support/scripts/lib/multiFunctions.ts b/packages/test-support/scripts/lib/multiFunctions.ts new file mode 100644 index 0000000000..707d2ec656 --- /dev/null +++ b/packages/test-support/scripts/lib/multiFunctions.ts @@ -0,0 +1,68 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; + +import { ApiPromise } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { aliceSigner } from '@polkadot/test-support/keyring'; + +import { waitForBountyState, waitForClaim } from './bountyWaitFunctions'; +import { acceptCurator, approveBounty, awardBounty, claimBounty, proposeBounty, proposeCurator } from './changeBountyStateFunctions'; +import { FUNDING_TIME, PAYOUT_TIME } from './constants'; + +export async function multiProposeBounty (api: ApiPromise, numberOfBounties: number): Promise { + const indexes = []; + + for (let i = 0; i < numberOfBounties; i++) { + indexes.push(await proposeBounty(api, new BN(500_000_000_000_000), `new bounty no ${i}`, aliceSigner())); + } + + return indexes; +} + +export async function multiApproveBounty (api: ApiPromise, bountyIndexes: number[]): Promise { + for (const bountyIndex of bountyIndexes) { + await approveBounty(api, bountyIndex, aliceSigner()); + } +} + +export async function multiWaitForBountyFunded (api: ApiPromise, bountyIndexes: number[]): Promise { + const waitFunctions = bountyIndexes.map((bountyIndex) => + waitForBountyState(api, 'isFunded', bountyIndex, { interval: 2000, timeout: FUNDING_TIME })); + + await Promise.all(waitFunctions); +} + +export async function multiProposeCurator (api: ApiPromise, bountyIndexes: number[], signers: KeyringPair[]): Promise { + for (let i = 0; i < bountyIndexes.length; i++) { + await proposeCurator(api, bountyIndexes[i], signers[i]); + } +} + +export async function multiAcceptCurator (api: ApiPromise, bountyIndexes: number[], signers: KeyringPair[]): Promise { + const acceptFunctions = bountyIndexes.map((bountyIndex, index) => + acceptCurator(api, bountyIndex, signers[index])); + + await Promise.all(acceptFunctions); +} + +export async function multiAwardBounty (api: ApiPromise, bountyIndexes: number[], signers: KeyringPair[]): Promise { + const awardFunctions = bountyIndexes.map((bountyIndex, index) => + awardBounty(api, bountyIndex, signers[index])); + + await Promise.all(awardFunctions); +} + +export async function multiWaitForClaim (api: ApiPromise, bountyIndexes: number[]): Promise { + for (const index of bountyIndexes) { + await waitForClaim(api, index, { interval: 2000, timeout: PAYOUT_TIME }); + } +} + +export async function multiClaimBounty (api: ApiPromise, bountyIndexes: number[], signers: KeyringPair[]): Promise { + const awardFunctions = bountyIndexes.map((bountyIndex, index) => + claimBounty(api, bountyIndex, signers[index])); + + await Promise.all(awardFunctions); +} diff --git a/packages/test-support/scripts/readme.md b/packages/test-support/scripts/readme.md new file mode 100644 index 0000000000..fee44cf505 --- /dev/null +++ b/packages/test-support/scripts/readme.md @@ -0,0 +1,18 @@ +## Scripts for bounty testing + +The scripts are prepared to run on a local, development version of substrate with following changes: +- `bin/node/runtime/src/lib.rs` + ``` + pub const SpendPeriod: BlockNumber = 1 * MINUTES; + pub const BountyDepositPayoutDelay: BlockNumber = 1 * MINUTES + ``` + +To run a script enter the `packages/test-support` directory and run: +``` +ts-node scripts/ +``` + +Available scripts: +- `createBounties` - creates a list of bounties, + one in each status ( Proposed, Funded, Curator Proposed, Active, Pending Payout, Closed ) + diff --git a/packages/test-support/src/api/createApi.ts b/packages/test-support/src/api/createApi.ts index 04dcccd538..adb4cba1ed 100644 --- a/packages/test-support/src/api/createApi.ts +++ b/packages/test-support/src/api/createApi.ts @@ -5,10 +5,10 @@ import { ApiPromise } from '@polkadot/api/promise'; import { WsProvider } from '@polkadot/rpc-provider'; import { SUBSTRATE_PORT } from '@polkadot/test-support/substrate'; -export async function createApi (): Promise { +export async function createApi (port: number = SUBSTRATE_PORT): Promise { process.env.NODE_ENV = 'test'; - const provider = new WsProvider(`ws://127.0.0.1:${SUBSTRATE_PORT}`); + const provider = new WsProvider(`ws://127.0.0.1:${port}`); const api = await ApiPromise.create({ provider }); diff --git a/packages/test-support/src/keyring/signers.ts b/packages/test-support/src/keyring/signers.ts index 552fb7688d..e7557c528c 100644 --- a/packages/test-support/src/keyring/signers.ts +++ b/packages/test-support/src/keyring/signers.ts @@ -9,3 +9,33 @@ export function aliceSigner (): KeyringPair { return keyring.addFromUri('//Alice'); } + +export function bobSigner (): KeyringPair { + const keyring = new Keyring({ type: 'sr25519' }); + + return keyring.addFromUri('//Bob'); +} + +export function charlieSigner (): KeyringPair { + const keyring = new Keyring({ type: 'sr25519' }); + + return keyring.addFromUri('//Charlie'); +} + +export function daveSigner (): KeyringPair { + const keyring = new Keyring({ type: 'sr25519' }); + + return keyring.addFromUri('//Dave'); +} + +export function eveSigner (): KeyringPair { + const keyring = new Keyring({ type: 'sr25519' }); + + return keyring.addFromUri('//Eve'); +} + +export function ferdieSigner (): KeyringPair { + const keyring = new Keyring({ type: 'sr25519' }); + + return keyring.addFromUri('//Ferdie'); +} diff --git a/packages/test-support/src/transaction/execute.ts b/packages/test-support/src/transaction/execute.ts index c8ffc78097..7f85c1c2bf 100644 --- a/packages/test-support/src/transaction/execute.ts +++ b/packages/test-support/src/transaction/execute.ts @@ -1,12 +1,12 @@ // Copyright 2017-2021 @polkadot/test-support authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { waitFor } from '@testing-library/react'; - import { SubmittableExtrinsic } from '@polkadot/api/types'; import { KeyringPair } from '@polkadot/keyring/types'; import { EventRecord, ExtrinsicStatus } from '@polkadot/types/interfaces'; +import { waitFor } from '../utils/waitFor'; + export async function execute (extrinsic: SubmittableExtrinsic<'promise'>, singer: KeyringPair, logger = { info: console.log }): Promise { let currentTxDone = false; @@ -36,5 +36,5 @@ export async function execute (extrinsic: SubmittableExtrinsic<'promise'>, singe } await extrinsic.signAndSend(singer, sendStatusCb); - await waitFor(() => expect(currentTxDone).toBeTruthy(), { timeout: 20000 }); + await waitFor(() => currentTxDone, { timeout: 20000 }); } diff --git a/packages/test-support/src/utils/waitFor.ts b/packages/test-support/src/utils/waitFor.ts new file mode 100644 index 0000000000..b8b6691859 --- /dev/null +++ b/packages/test-support/src/utils/waitFor.ts @@ -0,0 +1,27 @@ +// Copyright 2017-2021 @polkadot/test-support authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export type WaitOptions = { interval?: number, timeout?: number }; + +export async function waitFor (predicate: () => boolean, { interval, timeout }: WaitOptions): Promise; +export async function waitFor (predicate: () => Promise, { interval, timeout }: WaitOptions): Promise; + +export async function waitFor (predicate: () => Promise | boolean, { interval = 500, timeout = 10000 } = {}): Promise { + const asyncPredicate = () => Promise.resolve(predicate()); + + let elapsed = 0; + + while (!(await asyncPredicate())) { + if (elapsed > timeout) { + throw Error('Timeout'); + } + + await sleep(interval); + elapsed += interval; + } + + return true; +} + +export const sleep = (ms: number):Promise => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/test-support/tsconfig.json b/packages/test-support/tsconfig.json new file mode 100644 index 0000000000..d723a1b762 --- /dev/null +++ b/packages/test-support/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "target": "es2018" + }, + "ts-node": { + "require": ["tsconfig-paths/register"] + } +} diff --git a/yarn.lock b/yarn.lock index c9354d2a92..84bb7af9b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2622,6 +2622,8 @@ __metadata: dependencies: "@testing-library/react": ^11.2.3 testcontainers: ^6.4.1 + ts-node: ^9.1.1 + tsconfig-paths: ^3.9.0 languageName: unknown linkType: soft @@ -4388,6 +4390,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 81b3b40b1529c4fbf75b12f7c3e6fb2dcce9e78072063babc169de9b4f40777788f3d2b04380f659ef676a756e03ccfbfe78adf4477353bda906295fa69dab89 + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -6406,6 +6415,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: babd307893abfb26d77ae11cb9d6b6cfa6d18c9cee435cf70b5a3fb44aa8d90c9ec26ea89cbb16e0a94b8d34f5fcaee164b90ed526cdd3158955673ab9652d01 + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.0": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -7062,6 +7078,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 81b5cd7ddde6f0ba2a532d434cfdca365aedd6cc62bb133e851e66e071d40382a30924a07c1034bd3d5a2e332146f64514b73c06fe2ebc0490a67f0c98da79fb + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -12224,6 +12247,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 2c780bab8409b865e8ee86697c599a2bf2765ec64d21eb67ccda27050e039f983feacad05a0d43aba3c966ea03d305d2612e94fec45474bcbc61181f57c5bb88 + languageName: node + linkType: hard + "makeerror@npm:1.0.x": version: 1.0.11 resolution: "makeerror@npm:1.0.11" @@ -16473,7 +16503,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.19, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.19": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.17, source-map-support@npm:^0.5.19, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.19": version: 0.5.19 resolution: "source-map-support@npm:0.5.19" dependencies: @@ -17671,6 +17701,27 @@ fsevents@~2.1.2: languageName: node linkType: hard +"ts-node@npm:^9.1.1": + version: 9.1.1 + resolution: "ts-node@npm:9.1.1" + dependencies: + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + source-map-support: ^0.5.17 + yn: 3.1.1 + peerDependencies: + typescript: ">=2.7" + bin: + ts-node: dist/bin.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: a90db4a342872cd0e7a80babdfcb15d2f7c06e700d735003098f7cc79db575c3380580c58a19ae0d0eaab553af083651d4237060c92170e6f8ac4e64693113ea + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.9.0": version: 3.9.0 resolution: "tsconfig-paths@npm:3.9.0" @@ -19176,6 +19227,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: bff63b80568d80c711670935427494dde47cdf97e8b04196b140ce0af519c81c5ee857eddad0caa8b422dd65aea0157bbfaacbb1546bebba623f0f383d5d9ae5 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"