diff --git a/cli/README.md b/cli/README.md index 3c7d65981b..e7ca81a09a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -79,6 +79,20 @@ When using the CLI for the first time there are a few common steps you might wan * [`joystream-cli api:setQueryNodeEndpoint [ENDPOINT]`](#joystream-cli-apisetquerynodeendpoint-endpoint) * [`joystream-cli api:setUri [URI]`](#joystream-cli-apiseturi-uri) * [`joystream-cli autocomplete [SHELL]`](#joystream-cli-autocomplete-shell) +* [`joystream-cli bounty:bounty BOUNTYID`](#joystream-cli-bountybounty-bountyid) +* [`joystream-cli bounty:bounties`](#joystream-cli-bountybounties) +* [`joystream-cli bounty:createBounty`](#joystream-cli-bountycreatebounty) +* [`joystream-cli bounty:cancelBounty BOUNTYID`](#joystream-cli-bountycancelbounty-bountyid) +* [`joystream-cli bounty:vetoBounty BOUNTYID`](#joystream-cli-bountyvetobounty-bountyid) +* [`joystream-cli bounty:entry ENTRYID`](#joystream-cli-bountyentry-entryid) +* [`joystream-cli bounty:entries`](#joystream-cli-bountyentries) +* [`joystream-cli bounty:fundBounty BOUNTYID AMOUNT`](#joystream-cli-bountyfundbounty-bountyid-amount) +* [`joystream-cli bounty:withdrawFunding BOUNTYID`](#joystream-cli-bountywithdrawfunding-bountyid) +* [`joystream-cli bounty:announceWorkEntry BOUNTYID`](#joystream-cli-bountyannounceworkentry-bountyid) +* [`joystream-cli bounty:withdrawWorkEntry BOUNTYID ENTRYID`](#joystream-cli-bountywithdrawworkentry-bountyid-entryid) +* [`joystream-cli bounty:submitWork BOUNTYID ENTRYID`](#joystream-cli-bountysubmitwork-bountyid-entryid) +* [`joystream-cli bounty:submitOracleJudgment BOUNTYID`](#joystream-cli-bountysubmitoraclejudgment-bountyid) +* [`joystream-cli bounty:withdrawWorkEntrantFunds BOUNTYID ENTRYID`](#joystream-cli-bountywithdrawworkentrantfunds-bountyid-entryid) * [`joystream-cli content:addCuratorToGroup [GROUPID] [CURATORID]`](#joystream-cli-contentaddcuratortogroup-groupid-curatorid) * [`joystream-cli content:channel CHANNELID`](#joystream-cli-contentchannel-channelid) * [`joystream-cli content:channels`](#joystream-cli-contentchannels) @@ -347,6 +361,199 @@ EXAMPLES _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v0.2.1/src/commands/autocomplete/index.ts)_ +## `joystream-cli bounty:bounty BOUNTYID` + +Show Bounty details by id. + +``` +USAGE + $ joystream-cli bounty:bounty BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty +``` + +## `joystream-cli bounty:bounties` + +List all existing bounties. + +``` +USAGE + $ joystream-cli bounty:bounties +``` + +## `joystream-cli bounty:createBounty` + +Create bounty by a member or council. + +``` +USAGE + $ joystream-cli bounty:createBounty + + +OPTIONS + -i, --input=input (required) Path to JSON file to use as input + --creatorContext=(Member|Council) Actor context to execute the command in (Member/Council) + --contract=(Perpetual|Limited) Contract type to use in Bounty is (Perpetual/Limited) + --funding=(Open|Closed) Funding type to use in Bounty is (Open/Closed) +``` +`contractTypeInput` property of JSON input should be emply in case of `--contract=Open` and +contain list of members allowed to submit work in case of `--contarct=Closed` +`oracle` property of JSON input should be undefined(omitted from the input) if oracle bounty +actor is `Council`, and a vaild memberId if oracle is `Member` +## `joystream-cli bounty:cancelBounty BOUNTYID` + +Cancel a bounty. + +``` +USAGE + $ joystream-cli bounty:cancelBounty BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty to cancel + +OPTIONS + --context=(Member|Council) Actor context to execute the command in (Member/Council) +``` + +## `joystream-cli bounty:vetoBounty BOUNTYID` + +Veto bounty by the Council. + +``` +USAGE + $ joystream-cli bounty:vetoBounty BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty +``` + +## `joystream-cli bounty:fundBounty BOUNTYID AMOUNT` + +Provide funding to bounty. + +``` +USAGE + $ joystream-cli bounty:fundBounty BOUNTYID AMOUNT + +ARGUMENTS + BOUNTYID ID of the Bounty to veto + AMOUNT Amount to be contributed towards bounty by funder + +OPTIONS + --funderContext=(Member|Council) Actor context to execute the command in (Member/Council) +``` + +## `joystream-cli bounty:withdrawFunding BOUNTYID` + +Withdraw funding from the bounty. + +``` +USAGE + $ joystream-cli bounty:withdrawFunding BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty + +OPTIONS + --funderContext=(Member|Council) Actor context to execute the command in (Member/Council) +``` + +## `joystream-cli bounty:entry ENTRYID` + +Show work entry details by id. + +``` +USAGE + $ joystream-cli bounty:entry ENTRYID + +ARGUMENTS + ENTRYID ID of the Work entry +``` + +## `joystream-cli bounty:entries` + +List all existing entries. + +``` +USAGE + $ joystream-cli bounty:entries +``` + +## `joystream-cli bounty:announceWorkEntry BOUNTYID` + +Announce work entry for a bounty. + +``` +USAGE + $ joystream-cli bounty:announceWorkEntry BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty +``` +This command will ask for member account and staking account. + +## `joystream-cli bounty:withdrawWorkEntry BOUNTYID ENTRYID` + +Withdraw work entry from a bounty. + +``` +USAGE + $ joystream-cli bounty:withdrawWorkEntry BOUNTYID ENTRYID + +ARGUMENTS + BOUNTYID ID of the Bounty + ENTRYID ID of the Work entry +``` +This command will ask for member account. + +## `joystream-cli bounty:submitWork BOUNTYID ENTRYID` + +Submit work for a bounty. + +``` +USAGE + $ joystream-cli bounty:submitWork BOUNTYID ENTRYID + +ARGUMENTS + BOUNTYID ID of the Bounty + ENTRYID ID of the Work entry + +OPTIONS + -i, --input=input (required) Path to work data JSON file to use as input +``` +This command will ask for member account. + +## `joystream-cli bounty:submitOracleJudgment BOUNTYID` + +Submit judgment for a bounty. + +``` +USAGE + $ joystream-cli bounty:submitOracleJudgment BOUNTYID + +ARGUMENTS + BOUNTYID ID of the Bounty + +OPTIONS + -i, --input=input (required) Path to judgments data JSON file to use as input + --oracleContext=(Member|Council) Actor context to execute the command in (Member/Council) +``` + +## `joystream-cli bounty:withdrawWorkEntrantFunds BOUNTYID ENTRYID` + +Withdraw work entrant funds from a bounty. + +``` +USAGE + $ joystream-cli bounty:withdrawWorkEntrantFunds BOUNTYID ENTRYID + +ARGUMENTS + BOUNTYID ID of the Bounty + ENTRYID ID of the Work entry +``` +This command will ask for member account. + ## `joystream-cli content:addCuratorToGroup [GROUPID] [CURATORID]` Add Curator to existing Curator Group. diff --git a/cli/examples/bounty/CreateBounty.json b/cli/examples/bounty/CreateBounty.json new file mode 100644 index 0000000000..1204be6ffe --- /dev/null +++ b/cli/examples/bounty/CreateBounty.json @@ -0,0 +1,13 @@ +{ + "title": "Video like feature", + "description": "Members can like the videos", + "discussionThread": "https://testnet.joystream.org/#/bounty", + "bannerImageUri": "", + "contractTypeInput": [], + "oracle": 3, + "cherry": 100, + "entrantStake": 100, + "fundingTypeInput": { "target": 20 }, + "workPeriod": 50, + "judgementPeriod": 50 +} diff --git a/cli/package.json b/cli/package.json index 707c1f6e64..d917350511 100644 --- a/cli/package.json +++ b/cli/package.json @@ -114,6 +114,9 @@ }, "content": { "description": "Interactions with content directory module - managing vidoes, channels, assets, categories and curator groups" + }, + "bounty": { + "description": "Bounty management - create bounties, submit work and win rewards" } } }, diff --git a/cli/src/Api.ts b/cli/src/Api.ts index d317dc5a62..68509ffc83 100644 --- a/cli/src/Api.ts +++ b/cli/src/Api.ts @@ -42,6 +42,7 @@ import { VideoCategory, } from '@joystream/types/content' import { ContentId, DataObject } from '@joystream/types/storage' +import { BountyId, EntryId, JSBounty as Bounty, Entry } from '@joystream/types/bounty' import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject, DocumentNode } from '@apollo/client/core' import fetch from 'cross-fetch' import { Maybe } from './graphql/generated/schema' @@ -95,7 +96,7 @@ export default class Api { // Get api for use-cases where no type augmentations are desirable public getUnaugmentedApi(): UnaugmentedApiPromise { - return (this._api as unknown) as UnaugmentedApiPromise + return this._api as unknown as UnaugmentedApiPromise } private static async initApi(apiUri: string = DEFAULT_API_URI, metadataCache: Record) { @@ -264,7 +265,7 @@ export default class Api { return membership.isEmpty ? null : await this.memberDetails(memberId, membership) } - protected async expectedMembershipById(memberId: MemberId): Promise { + async expectedMembershipById(memberId: MemberId): Promise { const member = await this.membershipById(memberId) if (!member) { throw new CLIError(`Expected member was not found by id: ${memberId.toString()}`) @@ -458,6 +459,31 @@ export default class Api { return this.entriesByIds(this._api.query.members.membershipById) } + // Bounty + async availableBounties(): Promise<[BountyId, Bounty][]> { + return await this.entriesByIds(this._api.query.bounty.bounties) + } + + async bountyById(bountyId: BountyId | number | string): Promise { + const exists = !!(await this._api.query.bounty.bounties.size(bountyId)).toNumber() + if (!exists) { + throw new CLIError(`Bonuty by id ${bountyId.toString()} not found!`) + } + return await this._api.query.bounty.bounties(bountyId) + } + + async entryById(entryId: EntryId | number | string): Promise { + const exists = !!(await this._api.query.bounty.entries.size(entryId)).toNumber() + if (!exists) { + throw new CLIError(`Bonuty by id ${entryId.toString()} not found!`) + } + return await this._api.query.bounty.entries(entryId) + } + + async availableEntries(): Promise<[EntryId, Entry][]> { + return await this.entriesByIds(this._api.query.bounty.entries) + } + // Content directory async availableChannels(): Promise<[ChannelId, Channel][]> { return await this.entriesByIds(this._api.query.content.channelById) diff --git a/cli/src/Types.ts b/cli/src/Types.ts index afe2a5cf32..1f9a843263 100644 --- a/cli/src/Types.ts +++ b/cli/src/Types.ts @@ -16,6 +16,8 @@ import { IVideoMetadata, IVideoCategoryMetadata, IChannelCategoryMetadata, + IBountyMetadata, + IBountyWorkData, } from '@joystream/metadata-protobuf' // KeyringPair type extended with mandatory "meta.name" @@ -177,6 +179,44 @@ export type ChannelCategoryInputParameters = IChannelCategoryMetadata export type VideoCategoryInputParameters = IVideoCategoryMetadata +export type FundingTypeLimited = { + minFundingAmount: number + maxFundingAmount: number + fundingPeriod: number +} + +export type FundingTypePrepetual = { + target: number +} + +export type BountyInputParameters = IBountyMetadata & { + // oracle should undefined if bounty actor is Council, and + // a valid member id if bounty actor is a Member + oracle?: number + // contractTypeInput should be emply list in case of Open contract and + // contain list of members allowed to submit work in case of Closed contarct + contractTypeInput: string[] + cherry: number + entrantStake: number + // TODO: can this be improved? + fundingType: FundingTypeLimited | FundingTypePrepetual + workPeriod: number + judgementPeriod: number +} + +export type BountyWorkDataInputParameters = IBountyWorkData + +export type Winner = { + reward: number +} + +export enum Rejected {} + +export type OracleJudgmentInputParameters = { + entryId: number + judgment: Winner | Rejected +}[] + type AnyNonObject = string | number | boolean | any[] | Long // JSONSchema utility types diff --git a/cli/src/base/AccountsCommandBase.ts b/cli/src/base/AccountsCommandBase.ts index 421139b719..e9226e1218 100644 --- a/cli/src/base/AccountsCommandBase.ts +++ b/cli/src/base/AccountsCommandBase.ts @@ -18,6 +18,7 @@ import { mnemonicGenerate } from '@polkadot/util-crypto' import { validateAddress } from '../helpers/validation' import slug from 'slug' import { Membership } from '@joystream/types/members' +import { CouncilMemberOf } from '@joystream/types/council' import BN from 'bn.js' const ACCOUNTS_DIRNAME = 'accounts' @@ -158,7 +159,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase { // Try adding and retrieving the keys in order to validate that the backup file is correct keyring.addFromJson(accountJsonObj) account = keyring.getPair(accountJsonObj.address) as NamedKeyringPair // We can be sure it's named, because we forced it before - } catch (e) { + } catch (e: any) { throw new CLIError(`Provided backup file is not valid (${e.message})`, { exit: ExitCodes.InvalidFile }) } @@ -325,6 +326,19 @@ export default abstract class AccountsCommandBase extends ApiCommandBase { } } + // TODO: check if the logic to validate Council is correct. + // get the context of council member that will take action on behalf of the Council + async getCouncilMemberContext(): Promise { + const member = await this.getRequiredMemberContext() + const councilMembers = await this.getOriginalApi().query.council.councilMembers() + + const councilMember = councilMembers.find((cm) => cm.membership_id === member.id) + if (councilMember === undefined) { + this.error('No council member controller key available!', { exit: ExitCodes.AccessDenied }) + } + return councilMember + } + async getRequiredMemberContext(): Promise { // TODO: Limit only to a set of members provided by the user? const allMembers = await this.getApi().allMembers() diff --git a/cli/src/base/BountyCommandBase.ts b/cli/src/base/BountyCommandBase.ts new file mode 100644 index 0000000000..723e4b6f00 --- /dev/null +++ b/cli/src/base/BountyCommandBase.ts @@ -0,0 +1,336 @@ +import { + AssuranceContractType, + BountyActor, + BountyId, + FundingType, + JSBounty as Bounty, + OracleJudgment, + OracleWorkEntryJudgment, +} from '@joystream/types/bounty' +import { flags } from '@oclif/command' +import { CLIError } from '@oclif/errors' +import ExitCodes from '../ExitCodes' +import { FundingTypeLimited, FundingTypePrepetual, OracleJudgmentInputParameters, Winner } from '../Types' +import { RolesCommandBase } from './WorkingGroupsCommandBase' + +const BOUNTY_ACTOR_CONTEXTS = ['Member', 'Council'] as const +const CONTRACT_TYPE_CONTEXTS = ['Open', 'Closed'] as const +const FUNDING_TYPE_CONTEXTS = ['Perpetual', 'Limited'] as const + +type BountyActorContext = typeof BOUNTY_ACTOR_CONTEXTS[number] +type ContractTypeContext = typeof CONTRACT_TYPE_CONTEXTS[number] +type FundingTypeContext = typeof FUNDING_TYPE_CONTEXTS[number] + +/** + * Abstract base class for commands related to bounty module. + */ +export default abstract class BountyCommandBase extends RolesCommandBase { + static bountyActorContextFlag = flags.enum({ + name: 'creatorContext', + required: false, + description: `Actor context to execute the command in (${BOUNTY_ACTOR_CONTEXTS.join('/')})`, + options: [...BOUNTY_ACTOR_CONTEXTS], + }) + + static contractTypeFlag = flags.enum({ + name: 'contractType', + required: false, + description: `Assurance contract type for bounty is (${CONTRACT_TYPE_CONTEXTS.join('/')})`, + options: [...CONTRACT_TYPE_CONTEXTS], + }) + + static fundingTypeFlag = flags.enum({ + name: 'fundingType', + required: false, + description: `Funding type for bounty is (${FUNDING_TYPE_CONTEXTS.join('/')})`, + options: [...FUNDING_TYPE_CONTEXTS], + }) + + async promptForCreatorContext( + message = 'Choose in which context you wish to execute the command' + ): Promise { + return this.simplePrompt({ + message, + type: 'list', + choices: BOUNTY_ACTOR_CONTEXTS.map((c) => ({ name: c, value: c })), + }) + } + + async promptForContractType( + message = 'Choose contract type to select who can submit work for the bounty' + ): Promise { + return this.simplePrompt({ + message, + type: 'list', + choices: CONTRACT_TYPE_CONTEXTS.map((c) => ({ name: c, value: c })), + }) + } + + async promptForFundingType( + message = 'Choose funding type for the bounty based on time limitation preferences' + ): Promise { + return this.simplePrompt({ + message, + type: 'list', + choices: FUNDING_TYPE_CONTEXTS.map((c) => ({ name: c, value: c })), + }) + } + + async validateAndPrepareOracleInput(oracle: number | undefined): Promise { + if (!oracle) { + return this.createType('BountyActor', { Council: null }) + } else { + // ensure that member id is valid + const oracleId = await this.createType('MemberId', oracle) + await this.getApi().expectedMembershipById(oracleId) + + return this.createType('BountyActor', { Member: oracle }) + } + } + + // validate funding type input passed as json object key value + async validateAndPrepareFundingTypeInput( + fundingType: typeof FUNDING_TYPE_CONTEXTS[number], + fundingInput: FundingTypePrepetual | FundingTypeLimited + ): Promise { + const target = (fundingInput as FundingTypePrepetual).target + const maxFundingAmount = (fundingInput as FundingTypeLimited).maxFundingAmount + const minFundingAmount = (fundingInput as FundingTypeLimited).minFundingAmount + const fundingPeriod = (fundingInput as FundingTypeLimited).fundingPeriod + + if (fundingType === 'Perpetual' && target > 0) { + return this.createType('FundingType', { Perpetual: { target } }) + } else if ( + maxFundingAmount > 0 && + minFundingAmount > 0 && + fundingPeriod > 0 && + maxFundingAmount > minFundingAmount + ) { + return this.createType('FundingType', { + Limited: { + max_funding_amount: maxFundingAmount, + min_funding_amount: minFundingAmount, + funding_period: fundingPeriod, + }, + }) + } else { + throw new CLIError('Invalid funding type', { exit: ExitCodes.InvalidInput }) + } + } + + // prepare 'AssuranceContractType' based on contract type input i.e. Open | Closed. if contract type + // is open then contractTypeInput field in json input to createBounty command supposed to be empty array. + // Otherwise it would contain list of member ids that can submit work. + async validateAndPrepareContractTypeInput( + contractType: typeof CONTRACT_TYPE_CONTEXTS[number], + contractInput: string[] + ): Promise { + if (contractType === 'Open') { + return this.createType('AssuranceContractType', { Open: null }) + } else { + if (contractInput.length === 0) { + throw new CLIError('Closed contract member list is empty') + } + if (contractInput.length > this.getOriginalApi().consts.bounty.closedContractSizeLimit.toNumber()) { + throw new CLIError(`Judging period cannot be zero`) + } + + // Checks that each member id id valid + contractInput.forEach(async (each) => { + const memberId = await this.createType('MemberId', each) + await this.getApi().expectedMembershipById(memberId) + }) + return this.createType('AssuranceContractType', { Closed: contractInput }) + } + } + + async validateAndPrepareOracleJudgement( + bounty: Bounty, + judgementInput: OracleJudgmentInputParameters + ): Promise { + const oracleJudgmentMap = {} as OracleJudgment + let rewardSumFromJudgment = 0 + + judgementInput.map(async ({ entryId, judgment }) => { + // ensure that entry exists + await this.getApi().entryById(entryId) + + let workEntryJudgment: OracleWorkEntryJudgment + const workEntryId = this.createType('EntryId', entryId) + + // if oracle judgment is Winner + if (judgment.hasOwnProperty('reward')) { + const reward = (judgment as Winner).reward + + if (reward === 0) { + throw new CLIError(`Judgment reward cannot be zero`) + } + // calculate total reward money + rewardSumFromJudgment += reward + + workEntryJudgment = this.createType( + 'OracleWorkEntryJudgment', + this.createType('OracleJudgment_Winner', { Winner: { reward } }) + ) + } + + // if oracle judgment is Rejected + workEntryJudgment = this.createType('OracleWorkEntryJudgment', { Rejected: null }) + // insert entry id, work entry judgment pair to oracleJudgmentMap + oracleJudgmentMap.set(workEntryId, workEntryJudgment) + }) + + if (rewardSumFromJudgment !== bounty.total_funding.toNumber()) { + throw new CLIError(`Total reward should be equal to total funding`) + } + return oracleJudgmentMap + } + + // ----------------------------------- + // BOUNTY STAGE CALCULATION FUNCTIONS + // ----------------------------------- + + async isFundingStage(bounty: Bounty): Promise { + const fundingPeriodIsNotExpired = !(await this.fundingPeriodExpired(bounty)) + return fundingPeriodIsNotExpired + } + + async isFundingExpiredStage(bounty: Bounty): Promise { + if (bounty.milestone.isOfType('Created')) { + const hasNoContributions = bounty.milestone.asType('Created').has_contributions.isFalse + const fundingPeriodIsExpired = await this.fundingPeriodExpired(bounty) + return fundingPeriodIsExpired && hasNoContributions + } + return false + } + + async isWorkSubmissionStage(bounty: Bounty): Promise { + // Funding period is over. Minimum funding reached. Work period is not expired. + if (bounty.milestone.isOfType('Created')) { + // Never expires + if (bounty.creation_params.funding_type.isOfType('Perpetual')) { + return false + } + const createdAt = bounty.milestone.asType('Created').created_at.toNumber() + const fundingPeriod = bounty.creation_params.funding_type.asType('Limited').funding_period.toNumber() + if ( + this.minimumFundingReached(bounty) && + (await this.fundingPeriodExpired(bounty)) && + !(await this.workPeriodExpired(bounty, createdAt + fundingPeriod)) + ) { + return true + } + } + + // Maximum funding reached. Work period is not expired. + if (bounty.milestone.isOfType('BountyMaxFundingReached')) { + const maxFundingReachedAt = bounty.milestone.asType('BountyMaxFundingReached').max_funding_reached_at.toNumber() + const workPeriodIsNotExpired = !(await this.workPeriodExpired(bounty, maxFundingReachedAt)) + return workPeriodIsNotExpired + } + + // Work in progress. Work period is not expired. + if (bounty.milestone.isOfType('WorkSubmitted')) { + const workPeriodStartedAt = bounty.milestone.asType('WorkSubmitted').work_period_started_at.toNumber() + const workPeriodIsNotExpired = !(await this.workPeriodExpired(bounty, workPeriodStartedAt)) + return workPeriodIsNotExpired + } + return false + } + + async isJudgmentStage(bounty: Bounty): Promise { + if (bounty.milestone.isOfType('WorkSubmitted')) { + const workPeriodStartedAt = bounty.milestone.asType('WorkSubmitted').work_period_started_at.toNumber() + const workPeriodExpired = await this.workPeriodExpired(bounty, workPeriodStartedAt) + const judgmentPeriodIsNotExpired = !(await this.judgmentPeriodExpired(bounty, workPeriodStartedAt)) + + return workPeriodExpired && judgmentPeriodIsNotExpired + } + return false + } + + async isSuccessfulBountyWithdrawalStage(bounty: Bounty): Promise { + if (bounty.milestone.isOfType('JudgmentSubmitted')) { + const successfulBounty = bounty.milestone.asType('JudgmentSubmitted').successful_bounty + return successfulBounty.isTrue + } + return false + } + + // --------------------------------- + // BOUNTY STAGE CALCULATION HELPERS + // --------------------------------- + + // Checks whether the minimum funding reached for the bounty. + protected minimumFundingReached(bounty: Bounty): boolean { + if (bounty.creation_params.funding_type.isOfType('Perpetual')) { + // There is no minimum for the perpetual + // funding type - only maximum (target) + return false + } + const minFundingAmount = bounty.creation_params.funding_type.asType('Limited').min_funding_amount.toNumber() + return bounty.total_funding.toNumber() >= minFundingAmount + } + + async fundingPeriodExpired(bounty: Bounty): Promise { + // Prepetual funding never expires + if (bounty.creation_params.funding_type.isOfType('Perpetual')) { + return false + } + // Ensure if limited funding has expired + const createdAt = bounty.milestone.asType('Created').created_at.toNumber() + const currentBlock = (await this.getOriginalApi().query.system.number()).toNumber() + const fundingPeriod = bounty.creation_params.funding_type.asType('Limited').funding_period.toNumber() + + return createdAt + fundingPeriod < currentBlock + } + + // Checks whether the work period expired by now starting from the provided block number. + async workPeriodExpired(bounty: Bounty, workPeriodStartedAt: number): Promise { + const currentBlock = (await this.getOriginalApi().query.system.number()).toNumber() + return workPeriodStartedAt + bounty.creation_params.work_period.toNumber() < currentBlock + } + + // Checks whether the judgment period expired by now when + // work period start from the provided block number. + async judgmentPeriodExpired(bounty: Bounty, workPeriodStartedAt: number): Promise { + const currentBlock = (await this.getOriginalApi().query.system.number()).toNumber() + return ( + workPeriodStartedAt + + bounty.creation_params.work_period.toNumber() + + bounty.creation_params.judging_period.toNumber() < + currentBlock + ) + } + + // ---------------------------------------------------------------- + + async ensureBountyCanBeCanceled(bountyId: BountyId, creator: BountyActor): Promise { + const bounty = await this.getApi().bountyById(bountyId) + + // Only check if the creator trying to cancel the bounty + // is Member, Council can always veto the bounty + if (creator.isOfType('Member') && bounty.creation_params.creator.asType('Member') !== creator.asType('Member')) { + throw new CLIError(`Provided bounty actor cannot cancel the bounty!`) + } + + if (!bounty.milestone.isOfType('Created') || !bounty.milestone.asType('Created').has_contributions) { + throw new CLIError(`Bounty cannot be cancelled at this stage`) + } + } + + async getBountyActor(context: typeof BOUNTY_ACTOR_CONTEXTS[number]): Promise<[BountyActor, string]> { + let bountyActorContext: [BountyActor, string] + + if (context === 'Member') { + const { id, membership } = await this.getRequiredMemberContext() + bountyActorContext = [this.createType('BountyActor', { Member: id }), membership.controller_account.toString()] + } else { + // TODO: check if the logic to validate Council is correct. + const council = await this.getCouncilMemberContext() + bountyActorContext = [this.createType('BountyActor', { Council: null }), council.membership_id.toString()] + } + + return bountyActorContext + } +} diff --git a/cli/src/base/UploadCommandBase.ts b/cli/src/base/UploadCommandBase.ts index 10588d3076..401504bc17 100644 --- a/cli/src/base/UploadCommandBase.ts +++ b/cli/src/base/UploadCommandBase.ts @@ -102,7 +102,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB let ffProbeMetadata: VideoFFProbeMetadata = {} try { ffProbeMetadata = await this.getVideoFFProbeMetadata(filePath) - } catch (e) { + } catch (e: any) { const message = e.message || e this.warn(`Failed to get video metadata via ffprobe (${message})`) } @@ -208,7 +208,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB maxBodyLength: fileSize, } await axios.put(uploadUrl, fileStream, config) - } catch (e) { + } catch (e: any) { progressBar.stop() const msg = (e.response && e.response.data && e.response.data.message) || e.message || e this.error(`Unexpected error when trying to upload a file: ${msg}`, { diff --git a/cli/src/commands/api/inspect.ts b/cli/src/commands/api/inspect.ts index 3c43726c1d..6f57a96ec3 100644 --- a/cli/src/commands/api/inspect.ts +++ b/cli/src/commands/api/inspect.ts @@ -80,7 +80,7 @@ export default class ApiInspect extends ApiCommandBase { return this.getUnaugmentedApi().query[apiModule][apiMethod].creator.meta } else { // Currently the only other optoin is api.consts - const method = (this.getUnaugmentedApi().consts[apiModule][apiMethod] as unknown) as AugmentedConst<'promise'> + const method = this.getUnaugmentedApi().consts[apiModule][apiMethod] as unknown as AugmentedConst<'promise'> return method.meta } } diff --git a/cli/src/commands/bounty/announceWorkEntry.ts b/cli/src/commands/bounty/announceWorkEntry.ts new file mode 100644 index 0000000000..4419e4d4b0 --- /dev/null +++ b/cli/src/commands/bounty/announceWorkEntry.ts @@ -0,0 +1,63 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' + +export default class AnnounceWorkEntryCommand extends BountyCommandBase { + static description = 'Announce work entry for a bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID for the bounty', + }, + ] + + async run() { + const { bountyId } = this.parse(AnnounceWorkEntryCommand).args + + const bounty = await this.getApi().bountyById(bountyId) + const memberContext = await this.getRequiredMemberContext() + // TODO: check if the function properly ensures + // TODO: staking requirements for this usecase + const stakingAccount = await this.promptForStakingAccount( + bounty.creation_params.entrant_stake, + memberContext.id, + memberContext.membership + ) + + if (bounty.creation_params.contract_type.isOfType('Closed')) { + const isMemberAllowedToToWork = Array.from(bounty.creation_params.contract_type.asType('Closed')).includes( + memberContext.id + ) + if (!isMemberAllowedToToWork) { + this.error('Member is not allowed to work in this Closed bounty', { exit: ExitCodes.ApiError }) + } + } + + const canWorkEntryBeAnnounced = await this.isWorkSubmissionStage(bounty) + if (!canWorkEntryBeAnnounced) { + this.error('Work entry cannot be announced in this stage', { exit: ExitCodes.ApiError }) + } + + this.jsonPrettyPrint(JSON.stringify({ memberId: memberContext.id, bountyId, stakingAccount })) + + await this.requireConfirmation('Do you confirm the the input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(memberContext.membership.controller_account.toString()), + 'bounty', + 'announceWorkEntry', + [memberContext.id, bountyId, stakingAccount] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'WorkEntryAnnounced') + this.log( + chalk.green( + `Work entry with id ${chalk.cyanBright(event?.data[1].toString())} for Bounty with id ${chalk.cyanBright( + event?.data[0].toString() + )} successfully created!` + ) + ) + } + } +} diff --git a/cli/src/commands/bounty/bounties.ts b/cli/src/commands/bounty/bounties.ts new file mode 100644 index 0000000000..3fff4712f3 --- /dev/null +++ b/cli/src/commands/bounty/bounties.ts @@ -0,0 +1,30 @@ +import BountyCommandBase from '../../base/BountyCommandBase' +import { displayTable } from '../../helpers/display' + +export default class BountiesCommand extends BountyCommandBase { + static description = 'List all existing bounties.' + + async run() { + const bounties = await this.getApi().availableBounties() + if (bounties.length > 0) { + displayTable( + bounties.map(([id, b]) => ({ + 'ID': id.toString(), + 'Bounty Creator': b.creation_params.creator.toString(), + 'Oracle': b.creation_params.oracle.toString(), + 'Contract Type': b.creation_params.contract_type.toString(), + 'Bounty Cherry': b.creation_params.cherry.toString(), + 'Entrant Stake': b.creation_params.entrant_stake.toString(), + 'Funding Type': b.creation_params.funding_type.toString(), + 'Work Period': b.creation_params.work_period.toString(), + 'Judging Period': b.creation_params.judging_period.toString(), + 'Total Funding': b.total_funding.toString(), + 'Milestone': b.milestone.toString(), + 'Active Work Entry Count': b.active_work_entry_count.toString(), + })) + ) + } else { + this.log('There are no bounties yet') + } + } +} diff --git a/cli/src/commands/bounty/bounty.ts b/cli/src/commands/bounty/bounty.ts new file mode 100644 index 0000000000..8c7e208763 --- /dev/null +++ b/cli/src/commands/bounty/bounty.ts @@ -0,0 +1,36 @@ +import BountyCommandBase from '../../base/BountyCommandBase' +import { displayCollapsedRow } from '../../helpers/display' + +export default class BountyCommand extends BountyCommandBase { + static description = 'Show Bounty details by id.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of the Bounty', + }, + ] + + async run() { + const { bountyId } = this.parse(BountyCommand).args + const bounty = await this.getApi().bountyById(bountyId) + if (bounty) { + displayCollapsedRow({ + 'ID': bountyId.toString(), + 'Bounty Creator': bounty.creation_params.creator.toString(), + 'Oracle': bounty.creation_params.oracle.toString(), + 'Contract Type': bounty.creation_params.contract_type.toString(), + 'Bounty Cherry': bounty.creation_params.cherry.toString(), + 'Entrant Stake': bounty.creation_params.entrant_stake.toString(), + 'Funding Type': bounty.creation_params.funding_type.toString(), + 'Work Period': bounty.creation_params.work_period.toString(), + 'Judging Period': bounty.creation_params.judging_period.toString(), + 'Total Funding': bounty.total_funding.toString(), + 'Milestone': bounty.milestone.toString(), + 'Active Work Entry Count': bounty.active_work_entry_count.toString(), + }) + } else { + this.error(`Bounty not found by channel id: "${bountyId}"!`) + } + } +} diff --git a/cli/src/commands/bounty/cancelBounty.ts b/cli/src/commands/bounty/cancelBounty.ts new file mode 100644 index 0000000000..198be020d4 --- /dev/null +++ b/cli/src/commands/bounty/cancelBounty.ts @@ -0,0 +1,45 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' + +export default class CreateBountyCommand extends BountyCommandBase { + static description = 'Cancel a bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of the Bounty to cancel', + }, + ] + + static flags = { + context: BountyCommandBase.bountyActorContextFlag, + } + + async run() { + let { context } = this.parse(CreateBountyCommand).flags + const { bountyId } = this.parse(CreateBountyCommand).args + + // Context + if (!context) { + context = await this.promptForCreatorContext() + } + + const [creator, creatorAddress] = await this.getBountyActor(context) + await this.ensureBountyCanBeCanceled(bountyId, creator) + + this.jsonPrettyPrint(JSON.stringify({ bountyId })) + + await this.requireConfirmation('Do you confirm the the bountId?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(creatorAddress), + 'bounty', + 'cancelBounty', + [creator, bountyId] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'BountyCanceled') + this.log(chalk.green(`Bounty with id ${chalk.cyanBright(event?.data[1].toString())} successfully canceled!`)) + } + } +} diff --git a/cli/src/commands/bounty/createBounty.ts b/cli/src/commands/bounty/createBounty.ts new file mode 100644 index 0000000000..11ecf1faf2 --- /dev/null +++ b/cli/src/commands/bounty/createBounty.ts @@ -0,0 +1,95 @@ +import { getInputJson } from '../../helpers/InputOutput' +import { BountyCreationParameters } from '@joystream/types/bounty' +import ExitCodes from '../../ExitCodes' +import { flags } from '@oclif/command' +import { CreateInterface } from '@joystream/types' +import { BountyInputSchema } from '../../json-schemas/Bounty' +import { BountyInputParameters } from '../../Types' +import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization' +import BountyCommandBase from '../../base/BountyCommandBase' +import chalk from 'chalk' +import { BountyMetadata } from '@joystream/metadata-protobuf' + +export default class CreateBountyCommand extends BountyCommandBase { + static description = 'Create bounty by member or council.' + static flags = { + creatorContext: BountyCommandBase.bountyActorContextFlag, + contract: BountyCommandBase.contractTypeFlag, + funding: BountyCommandBase.fundingTypeFlag, + input: flags.string({ + char: 'i', + required: true, + description: `Path to JSON file to use as input`, + }), + } + + async run() { + let { creatorContext, contract, funding, input } = this.parse(CreateBountyCommand).flags + + // TODO: is there a better way to to get the input wothout the prompt? + // TODO: maybe defining'enum' json schema? and then passing as input file + // Context + if (!creatorContext) { + creatorContext = await this.promptForCreatorContext() + } + + // Contract Type + if (!contract) { + contract = await this.promptForContractType() + } + + // Funding Type + if (!funding) { + funding = await this.promptForFundingType() + } + + const [creator, creatorAddress] = await this.getBountyActor(creatorContext) + + const bountyInput = await getInputJson(input, BountyInputSchema) + const metadata = asValidatedMetadata(BountyMetadata, bountyInput) + + const oracle = await this.validateAndPrepareOracleInput(bountyInput.oracle) + const contractType = await this.validateAndPrepareContractTypeInput(contract, bountyInput.contractTypeInput) + const fundingType = await this.validateAndPrepareFundingTypeInput(funding, bountyInput.fundingType) + + // Do remaining validations on input + if (bountyInput.cherry < this.getOriginalApi().consts.bounty.minCherryLimit.toNumber()) { + this.error('Attached cherry is less that minimum cherry limit', { exit: ExitCodes.InvalidInput }) + } + if (bountyInput.entrantStake < this.getOriginalApi().consts.bounty.minWorkEntrantStake.toNumber()) { + this.error('Attached entrant stake is less that minimum work entrant stake', { exit: ExitCodes.InvalidInput }) + } + if (bountyInput.workPeriod !== 0) { + this.error('Work period cannot be zero', { exit: ExitCodes.InvalidInput }) + } + if (bountyInput.judgementPeriod !== 0) { + this.error('Judging period cannot be zero', { exit: ExitCodes.InvalidInput }) + } + + const bountyCreationParameters: CreateInterface = { + oracle, + contract_type: contractType, + creator, + cherry: bountyInput.cherry, + entrant_stake: bountyInput.entrantStake, + funding_type: fundingType, + work_period: bountyInput.workPeriod, + judging_period: bountyInput.judgementPeriod, + } + + this.jsonPrettyPrint(JSON.stringify({ metadata })) + + await this.requireConfirmation('Do you confirm the provided input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(creatorAddress), + 'bounty', + 'createBounty', + [bountyCreationParameters, metadataToBytes(BountyMetadata, metadata)] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'BountyCreated') + this.log(chalk.green(`Bounty with id ${chalk.cyanBright(event?.data[1].toString())} successfully created!`)) + } + } +} diff --git a/cli/src/commands/bounty/entries.ts b/cli/src/commands/bounty/entries.ts new file mode 100644 index 0000000000..47b6b96024 --- /dev/null +++ b/cli/src/commands/bounty/entries.ts @@ -0,0 +1,24 @@ +import BountyCommandBase from '../../base/BountyCommandBase' +import { displayTable } from '../../helpers/display' + +export default class EntriesCommand extends BountyCommandBase { + static description = 'List all existing work entries.' + + async run() { + const bounties = await this.getApi().availableEntries() + if (bounties.length > 0) { + displayTable( + bounties.map(([id, e]) => ({ + 'ID': id.toString(), + 'Member ID': e.member_id.toString(), + 'Staking Account ID': e.staking_account_id.toString(), + 'Submitted At': e.submitted_at.toString(), + 'Work Submitted': e.work_submitted.toString(), + 'Oracle Judgement': e.oracle_judgment_result.unwrapOr('None').toString(), + })) + ) + } else { + this.log('There are no work entries yet') + } + } +} diff --git a/cli/src/commands/bounty/entry.ts b/cli/src/commands/bounty/entry.ts new file mode 100644 index 0000000000..3a4e4e24d3 --- /dev/null +++ b/cli/src/commands/bounty/entry.ts @@ -0,0 +1,30 @@ +import BountyCommandBase from '../../base/BountyCommandBase' +import { displayCollapsedRow } from '../../helpers/display' + +export default class EntryCommand extends BountyCommandBase { + static description = 'Show Work entry details by id.' + static args = [ + { + name: 'entryId', + required: true, + description: 'ID of the work entry', + }, + ] + + async run() { + const { entryId } = this.parse(EntryCommand).args + const entry = await this.getApi().entryById(entryId) + if (entry) { + displayCollapsedRow({ + 'ID': entryId.toString(), + 'Member ID': entry.member_id.toString(), + 'Staking Account ID': entry.staking_account_id.toString(), + 'Submitted At': entry.submitted_at.toString(), + 'Work Submitted': entry.work_submitted.toString(), + 'Oracle Judgement': entry.oracle_judgment_result.unwrapOr('None').toString(), + }) + } else { + this.error(`Work entry not found by channel id: "${entryId}"!`) + } + } +} diff --git a/cli/src/commands/bounty/fundBounty.ts b/cli/src/commands/bounty/fundBounty.ts new file mode 100644 index 0000000000..2ee45bd821 --- /dev/null +++ b/cli/src/commands/bounty/fundBounty.ts @@ -0,0 +1,63 @@ +import BN from 'bn.js' +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' +import { checkBalance, isValidBalance } from '../../helpers/validation' + +export default class FundBountyCommand extends BountyCommandBase { + static description = 'Provide funding to bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of the Bounty to fund', + }, + { + name: 'amount', + required: true, + description: 'Amount to be contributed towards bounty by funder', + }, + ] + + static flags = { + funderContext: BountyCommandBase.bountyActorContextFlag, + } + + async run() { + let { funderContext } = this.parse(FundBountyCommand).flags + const { bountyId, amount } = this.parse(FundBountyCommand).args + + // Context + if (!funderContext) { + funderContext = await this.promptForCreatorContext() + } + + const [funder, funderAddress] = await this.getBountyActor(funderContext) + const bounty = await this.getApi().bountyById(bountyId) + const funderBalance = (await this.getApi().getAccountSummary(funderAddress)).balances + const amountBN = new BN(amount) + + if (!isValidBalance(amount) || amountBN < this.getOriginalApi().consts.bounty.minFundingLimit.toBn()) { + this.error('Funding input is invalid or less than the minimum funding limit', { exit: ExitCodes.InvalidInput }) + } + + checkBalance(funderBalance, amountBN) + if (await this.fundingPeriodExpired(bounty)) { + this.error('Funding period expired', { exit: ExitCodes.ApiError }) + } + + this.jsonPrettyPrint(JSON.stringify({ bountyId, amount })) + + await this.requireConfirmation('Do you confirm the the input?', true) + + const result = await this.sendAndFollowNamedTx(await this.getDecodedPair(funderAddress), 'bounty', 'fundBounty', [ + funder, + bountyId, + amount, + ]) + if (result) { + const event = this.findEvent(result, 'bounty', 'BountyCanceled') + this.log(chalk.green(`Bounty with id ${chalk.cyanBright(event?.data[1].toString())} successfully funded!`)) + } + } +} diff --git a/cli/src/commands/bounty/submitOracleJudgment.ts b/cli/src/commands/bounty/submitOracleJudgment.ts new file mode 100644 index 0000000000..94fa87edd9 --- /dev/null +++ b/cli/src/commands/bounty/submitOracleJudgment.ts @@ -0,0 +1,75 @@ +import { flags } from '@oclif/command' +import chalk from 'chalk' +import { OracleJudgmentInputSchema } from '../../json-schemas/Bounty' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' +import { getInputJson } from '../../helpers/InputOutput' +import { OracleJudgmentInputParameters } from '../../Types' + +export default class SubmitOracleJudgmentCommand extends BountyCommandBase { + static description = 'Submit judgment for a bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of bounty for which judgement is being submitted.', + }, + ] + + static flags = { + oracleContext: BountyCommandBase.bountyActorContextFlag, + input: flags.string({ + char: 'i', + required: true, + description: `Path to oracle judgment JSON file to use as input`, + }), + } + + async run() { + let { oracleContext, input } = this.parse(SubmitOracleJudgmentCommand).flags + const { bountyId } = this.parse(SubmitOracleJudgmentCommand).args + + // Context + if (!oracleContext) { + oracleContext = await this.promptForCreatorContext() + } + + const [oracle, oracleAddress] = await this.getBountyActor(oracleContext) + const bounty = await this.getApi().bountyById(bountyId) + const judgmentInput = await getInputJson(input, OracleJudgmentInputSchema) + const oracleJudgment = await this.validateAndPrepareOracleJudgement(bounty, judgmentInput) + + if (bounty.creation_params.oracle !== oracle) { + this.error('Bounty actor is not allowed to act as oracle', { exit: ExitCodes.AccessDenied }) + } + + if (!(await this.isJudgmentStage(bounty))) { + this.error('Judgment cannot be submitted in this stage', { exit: ExitCodes.ApiError }) + } + + if (bounty.active_work_entry_count.toNumber() === 0) { + this.error('No active work entires exist', { exit: ExitCodes.ApiError }) + } + + this.jsonPrettyPrint(JSON.stringify({ oracle, bountyId, oracleJudgment })) + + await this.requireConfirmation('Do you confirm the provided input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(oracleAddress), + 'bounty', + 'submitOracleJudgment', + [oracle, bountyId, oracleJudgment] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'OracleJudgmentSubmitted') + this.log( + chalk.green( + `Oracle judgment for bounty with id ${chalk.cyanBright( + event?.data[0].toString() + )} was successfully submitted!` + ) + ) + } + } +} diff --git a/cli/src/commands/bounty/submitWork.ts b/cli/src/commands/bounty/submitWork.ts new file mode 100644 index 0000000000..3a40e74388 --- /dev/null +++ b/cli/src/commands/bounty/submitWork.ts @@ -0,0 +1,70 @@ +import { BountyWorkData } from '@joystream/metadata-protobuf' +import { flags } from '@oclif/command' +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' +import { getInputJson } from '../../helpers/InputOutput' +import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization' +import { BountyWorkDataInputSchema } from '../../json-schemas/Bounty' +import { BountyWorkDataInputParameters } from '../../Types' + +export default class SubmitWorkCommand extends BountyCommandBase { + static description = 'Submit work for a bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID for the bounty', + }, + { + name: 'entryId', + required: true, + description: 'ID for work entry.', + }, + ] + + static flags = { + input: flags.string({ + char: 'i', + required: true, + description: `Path to work data JSON file to use as input`, + }), + } + + async run() { + const { input } = this.parse(SubmitWorkCommand).flags + const { bountyId, entryId } = this.parse(SubmitWorkCommand).args + + const bounty = await this.getApi().bountyById(bountyId) + // ensure that entry exists + await this.getApi().entryById(entryId) + const memberContext = await this.getRequiredMemberContext() + const canWorkbeSubmitted = await this.isWorkSubmissionStage(bounty) + + if (!canWorkbeSubmitted) { + this.error('Work cannot be submitted in this stage', { exit: ExitCodes.ApiError }) + } + + const workDataInput = await getInputJson(input, BountyWorkDataInputSchema) + const workData = asValidatedMetadata(BountyWorkData, workDataInput) + + this.jsonPrettyPrint(JSON.stringify({ memberId: memberContext.id, bountyId, entryId })) + + await this.requireConfirmation('Do you confirm the the input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(memberContext.membership.controller_account.toString()), + 'bounty', + 'submitWork', + [memberContext.id, bountyId, entryId, metadataToBytes(BountyWorkData, workData)] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'WorkSubmitted') + this.log( + chalk.green( + `Work data successfully submitted for bounty with id ${chalk.cyanBright(event?.data[0].toString())}` + ) + ) + } + } +} diff --git a/cli/src/commands/bounty/vetoBounty.ts b/cli/src/commands/bounty/vetoBounty.ts new file mode 100644 index 0000000000..660924db52 --- /dev/null +++ b/cli/src/commands/bounty/vetoBounty.ts @@ -0,0 +1,35 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' + +export default class VetoBountyCommand extends BountyCommandBase { + static description = 'Veto bounty by the Council.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of the Bounty to veto', + }, + ] + + async run() { + const { bountyId } = this.parse(VetoBountyCommand).args + + const [councilMember, councilMemberAddress] = await this.getBountyActor('Council') + await this.ensureBountyCanBeCanceled(bountyId, councilMember) + + this.jsonPrettyPrint(JSON.stringify({ bountyId })) + + await this.requireConfirmation('Do you confirm the the bountId?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(councilMemberAddress), + 'bounty', + 'vetoBounty', + [bountyId] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'BountyVetoed') + this.log(chalk.green(`Bounty with id ${chalk.cyanBright(event?.data[1].toString())} successfully vetoed!`)) + } + } +} diff --git a/cli/src/commands/bounty/withdrawFunding.ts b/cli/src/commands/bounty/withdrawFunding.ts new file mode 100644 index 0000000000..eecf6b207a --- /dev/null +++ b/cli/src/commands/bounty/withdrawFunding.ts @@ -0,0 +1,65 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' + +export default class WithdrawFundingCommand extends BountyCommandBase { + static description = 'Withdraw funding from the bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'ID of the Bounty', + }, + ] + + static flags = { + funderContext: BountyCommandBase.bountyActorContextFlag, + } + + async run() { + let { funderContext } = this.parse(WithdrawFundingCommand).flags + const { bountyId } = this.parse(WithdrawFundingCommand).args + + // Context + if (!funderContext) { + funderContext = await this.promptForCreatorContext() + } + + const [funder, funderAddress] = await this.getBountyActor(funderContext) + const bounty = await this.getApi().bountyById(bountyId) + + const isFundingStage = await this.isFundingStage(bounty) + const isFundingExpiredStage = await this.isFundingExpiredStage(bounty) + const isWorkSubmissionStage = await this.isWorkSubmissionStage(bounty) + const isJudgmentStage = await this.isJudgmentStage(bounty) + const isSuccessfulBountyWithdrawlStage = await this.isSuccessfulBountyWithdrawalStage(bounty) + + // if bounty is in any of the following stages funds cannot be withdrawn + if ( + isFundingStage || + isFundingExpiredStage || + isWorkSubmissionStage || + isJudgmentStage || + isSuccessfulBountyWithdrawlStage + ) { + this.error('Funds cannot be withdrawn in this stage', { exit: ExitCodes.AccessDenied }) + } + + this.jsonPrettyPrint(JSON.stringify({ bountyId })) + + await this.requireConfirmation('Do you confirm to withdraw funding from bounty?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(funderAddress), + 'bounty', + 'withdrawFunding', + [funder, bountyId] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'BountyFundingWithdrawal') + this.log( + chalk.green(`Amount successfully refunded from bounty with id ${chalk.cyanBright(event?.data[1].toString())} !`) + ) + } + } +} diff --git a/cli/src/commands/bounty/withdrawWorkEntrantFunds.ts b/cli/src/commands/bounty/withdrawWorkEntrantFunds.ts new file mode 100644 index 0000000000..90a17458f1 --- /dev/null +++ b/cli/src/commands/bounty/withdrawWorkEntrantFunds.ts @@ -0,0 +1,57 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' + +export default class WithdrawWorkEntrantCommand extends BountyCommandBase { + static description = 'Cashout work entrant funds.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'Identifier for bounty to which work entry corresponds.', + }, + { + name: 'entryId', + required: true, + description: 'Identifier for work entry.', + }, + ] + + async run() { + const { bountyId, entryId } = this.parse(WithdrawWorkEntrantCommand).args + + const bounty = await this.getApi().bountyById(bountyId) + await this.getApi().entryById(entryId) + // Get member context + const memberContext = await this.getRequiredMemberContext() + + const isFundingStage = await this.isFundingStage(bounty) + const isFundingExpiredStage = await this.isFundingExpiredStage(bounty) + const isWorkSubmissionStage = await this.isWorkSubmissionStage(bounty) + const isJudgmentStage = await this.isJudgmentStage(bounty) + + // if bounty is in any of the following stages work entrant funds cannot be withdrawn + if (isFundingStage || isFundingExpiredStage || isWorkSubmissionStage || isJudgmentStage) { + this.error('Work entrant funds cannot be withdrawn in this stage', { exit: ExitCodes.AccessDenied }) + } + + this.jsonPrettyPrint(JSON.stringify({ memberId: memberContext.id, bountyId, entryId })) + + await this.requireConfirmation('Do you confirm the the input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(memberContext.membership.controller_account.toString()), + 'bounty', + 'withdrawWorkEntrantFunds', + [memberContext.id, bountyId, entryId] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'WorkEntrantFundsWithdrawn') + this.log( + chalk.green( + `Work entrant funds successfully cashed out for bounty with id ${chalk.cyanBright(event?.data[0].toString())}` + ) + ) + } + } +} diff --git a/cli/src/commands/bounty/withdrawWorkEntry.ts b/cli/src/commands/bounty/withdrawWorkEntry.ts new file mode 100644 index 0000000000..b3b81891bb --- /dev/null +++ b/cli/src/commands/bounty/withdrawWorkEntry.ts @@ -0,0 +1,49 @@ +import chalk from 'chalk' +import BountyCommandBase from '../../base/BountyCommandBase' +import ExitCodes from '../../ExitCodes' + +export default class AnnounceWorkEntryCommand extends BountyCommandBase { + static description = 'Withdraw work entry from a bounty.' + static args = [ + { + name: 'bountyId', + required: true, + description: 'Identifier for bounty from which to withdraw work entry.', + }, + { + name: 'entryId', + required: true, + description: 'Identifier for work entry.', + }, + ] + + async run() { + const { bountyId, entryId } = this.parse(AnnounceWorkEntryCommand).args + + const bounty = await this.getApi().bountyById(bountyId) + await this.getApi().entryById(entryId) + const memberContext = await this.getRequiredMemberContext() + const canWorkEntryBeAnnounced = await this.isWorkSubmissionStage(bounty) + + if (!canWorkEntryBeAnnounced) { + this.error('Work entry cannot be withdrawn in this stage', { exit: ExitCodes.ApiError }) + } + + this.jsonPrettyPrint(JSON.stringify({ memberId: memberContext.id, bountyId, entryId })) + + await this.requireConfirmation('Do you confirm the the input?', true) + + const result = await this.sendAndFollowNamedTx( + await this.getDecodedPair(memberContext.membership.controller_account.toString()), + 'bounty', + 'withdrawWorkEntry', + [memberContext.id, bountyId, entryId] + ) + if (result) { + const event = this.findEvent(result, 'bounty', 'WorkEntryWithdrawn') + this.log( + chalk.green(`Work entry with id ${chalk.cyanBright(event?.data[1].toString())} successfully withdrawn!`) + ) + } + } +} diff --git a/cli/src/json-schemas/Bounty.ts b/cli/src/json-schemas/Bounty.ts new file mode 100644 index 0000000000..5dd6babe2f --- /dev/null +++ b/cli/src/json-schemas/Bounty.ts @@ -0,0 +1,173 @@ +// schema are generated by Typescript JSON schema generator VSCode extension + +export const BountyInputSchema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$ref': '#/definitions/BountyInputParameters', + 'definitions': { + 'BountyInputParameters': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'oracle': { + 'type': 'number', + }, + 'contractTypeInput': { + 'type': 'array', + 'items': { + 'type': 'string', + }, + }, + 'cherry': { + 'type': 'number', + }, + 'entrantStake': { + 'type': 'number', + }, + 'fundingType': { + 'anyOf': [ + { + '$ref': '#/definitions/FundingTypeLimited', + }, + { + '$ref': '#/definitions/FundingTypePrepetual', + }, + ], + }, + 'workPeriod': { + 'type': 'number', + }, + 'judgementPeriod': { + 'type': 'number', + }, + 'title': { + 'type': ['string', 'null'], + }, + 'description': { + 'type': ['string', 'null'], + }, + 'discussionThread': { + 'anyOf': [ + { + '$ref': '#/definitions/Long.Long', + }, + { + 'type': 'null', + }, + ], + }, + 'bannerImageUri': { + 'type': ['string', 'null'], + }, + }, + 'required': ['cherry', 'contractTypeInput', 'entrantStake', 'fundingType', 'judgementPeriod', 'workPeriod'], + }, + 'FundingTypeLimited': { + 'type': 'object', + 'properties': { + 'minFundingAmount': { + 'type': 'number', + }, + 'maxFundingAmount': { + 'type': 'number', + }, + 'fundingPeriod': { + 'type': 'number', + }, + }, + 'required': ['minFundingAmount', 'maxFundingAmount', 'fundingPeriod'], + 'additionalProperties': false, + }, + 'FundingTypePrepetual': { + 'type': 'object', + 'properties': { + 'target': { + 'type': 'number', + }, + }, + 'required': ['target'], + 'additionalProperties': false, + }, + 'Long.Long': { + 'type': 'object', + 'properties': { + 'high': { + 'type': 'number', + }, + 'low': { + 'type': 'number', + }, + 'unsigned': { + 'type': 'boolean', + }, + }, + 'required': ['high', 'low', 'unsigned'], + 'additionalProperties': false, + }, + }, +} + +export const BountyWorkDataInputSchema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$ref': '#/definitions/BountyWorkDataInputParameters', + 'definitions': { + 'BountyWorkDataInputParameters': { + '$ref': '#/definitions/IBountyWorkData', + }, + 'IBountyWorkData': { + 'type': 'object', + 'properties': { + 'title': { + 'type': ['string', 'null'], + }, + 'description': { + 'type': ['string', 'null'], + }, + }, + 'additionalProperties': false, + }, + }, +} + +export const OracleJudgmentInputSchema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$ref': '#/definitions/OracleJudgmentInputParameters', + 'definitions': { + 'OracleJudgmentInputParameters': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'entryId': { + 'type': 'number', + }, + 'judgment': { + 'anyOf': [ + { + '$ref': '#/definitions/Winner', + }, + { + '$ref': '#/definitions/Rejected', + }, + ], + }, + }, + 'required': ['entryId', 'judgment'], + 'additionalProperties': false, + }, + }, + 'Winner': { + 'type': 'object', + 'properties': { + 'reward': { + 'type': 'number', + }, + }, + 'required': ['reward'], + 'additionalProperties': false, + }, + 'Rejected': { + 'type': [], + 'enum': [], + }, + }, +} diff --git a/types/augment-codec/all.ts b/types/augment-codec/all.ts index 6ca594129c..0a93d96c50 100644 --- a/types/augment-codec/all.ts +++ b/types/augment-codec/all.ts @@ -9,7 +9,7 @@ import { ProposalId, ProposalStatus, Proposal as ProposalOf, ProposalDetails, Pr import { ReferendumStageVoting, ReferendumStageRevealing, ReferendumStage, OptionResult, VotePower } from '../referendum'; import { ConstitutionInfo } from '../constitution'; import { ParticipantId, Title, UpdatedTitle, UpdatedBody, ReplyId, Reply, ReplyToDelete } from '../blog'; -import { BountyId, EntryId, BountyActor, AssuranceContractType, FundingType_Limited, FundingType_Perpetual, FundingType, BountyCreationParameters, OracleJudgment_Winner, OracleJudgment, Entry } from '../bounty'; +import { BountyId, JSBounty, BountyMilestone, EntryId, BountyActor, AssuranceContractType, FundingType_Limited, FundingType_Perpetual, FundingType, BountyCreationParameters, OracleJudgment_Winner, OracleWorkEntryJudgment, OracleJudgment, Entry } from '../bounty'; import { CuratorId, CuratorGroupId, CuratorGroup, ContentActor, NewAsset, Channel, ChannelOwner, ChannelCategoryId, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelUpdateParameters, ChannelOwnershipTransferRequestId, ChannelOwnershipTransferRequest, Video, VideoId, VideoCategoryId, VideoCategory, VideoCategoryCreationParameters, VideoCategoryUpdateParameters, VideoCreationParameters, VideoUpdateParameters, Person, PersonId, PersonController, PersonActor, PersonCreationParameters, PersonUpdateParameters, Playlist, PlaylistId, PlaylistCreationParameters, PlaylistUpdateParameters, SeriesId, Series, Season, SeriesParameters, SeasonParameters, EpisodeParemters, MaxNumber, IsCensored } from '../content'; -export { ActorId, MemberId, BlockAndTime, ThreadId, PostId, InputValidationLengthConstraint, WorkingGroup, MemoText, BalanceKind, Address, LookupSource, ChannelId, DAOId, Url, Membership, StakingAccountMemberBinding, BuyMembershipParameters, InviteMembershipParameters, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CouncilStage, Candidate, CouncilMemberOf, CastVoteOf, ForumUserId, ModeratorId, CategoryId, PostReactionId, Category, Thread, Post, PollAlternative, Poll, PrivilegedActor, PollInput, ThreadOf, ExtendedPostId, ApplicationId, Application, ApplicationInfo, ApplicationIdSet, ApplicationIdToWorkerIdMap, WorkerId, Worker, WorkerInfo, Opening, OpeningId, StakePolicy, StakeParameters, StorageProviderId, OpeningType, ApplyOnOpeningParameters, Penalty, RewardPaymentType, ContentId, LiaisonJudgement, DataObject, DataObjectStorageRelationshipId, DataObjectStorageRelationship, DataObjectTypeId, DataObjectType, DataObjectsMap, ContentParameters, StorageObjectOwner, ObjectOwner, Voucher, VoucherLimit, UploadingStatus, ProposalId, ProposalStatus, ProposalOf, ProposalDetails, ProposalDetailsOf, VotingResults, ProposalParameters, GeneralProposalParameters, VoteKind, DiscussionThread, DiscussionPost, CreateOpeningParameters, FillOpeningParameters, TerminateRoleParameters, ProposalDecision, ExecutionFailed, Approved, SetLeadParams, ThreadMode, ExecutionStatus, FundingRequestParameters, ReferendumStageVoting, ReferendumStageRevealing, ReferendumStage, OptionResult, VotePower, ConstitutionInfo, ParticipantId, Title, UpdatedTitle, UpdatedBody, ReplyId, Reply, ReplyToDelete, BountyId, EntryId, BountyActor, AssuranceContractType, FundingType_Limited, FundingType_Perpetual, FundingType, BountyCreationParameters, OracleJudgment_Winner, OracleJudgment, Entry, CuratorId, CuratorGroupId, CuratorGroup, ContentActor, NewAsset, Channel, ChannelOwner, ChannelCategoryId, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelUpdateParameters, ChannelOwnershipTransferRequestId, ChannelOwnershipTransferRequest, Video, VideoId, VideoCategoryId, VideoCategory, VideoCategoryCreationParameters, VideoCategoryUpdateParameters, VideoCreationParameters, VideoUpdateParameters, Person, PersonId, PersonController, PersonActor, PersonCreationParameters, PersonUpdateParameters, Playlist, PlaylistId, PlaylistCreationParameters, PlaylistUpdateParameters, SeriesId, Series, Season, SeriesParameters, SeasonParameters, EpisodeParemters, MaxNumber, IsCensored }; \ No newline at end of file +export { ActorId, MemberId, BlockAndTime, ThreadId, PostId, InputValidationLengthConstraint, WorkingGroup, MemoText, BalanceKind, Address, LookupSource, ChannelId, DAOId, Url, Membership, StakingAccountMemberBinding, BuyMembershipParameters, InviteMembershipParameters, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CouncilStage, Candidate, CouncilMemberOf, CastVoteOf, ForumUserId, ModeratorId, CategoryId, PostReactionId, Category, Thread, Post, PollAlternative, Poll, PrivilegedActor, PollInput, ThreadOf, ExtendedPostId, ApplicationId, Application, ApplicationInfo, ApplicationIdSet, ApplicationIdToWorkerIdMap, WorkerId, Worker, WorkerInfo, Opening, OpeningId, StakePolicy, StakeParameters, StorageProviderId, OpeningType, ApplyOnOpeningParameters, Penalty, RewardPaymentType, ContentId, LiaisonJudgement, DataObject, DataObjectStorageRelationshipId, DataObjectStorageRelationship, DataObjectTypeId, DataObjectType, DataObjectsMap, ContentParameters, StorageObjectOwner, ObjectOwner, Voucher, VoucherLimit, UploadingStatus, ProposalId, ProposalStatus, ProposalOf, ProposalDetails, ProposalDetailsOf, VotingResults, ProposalParameters, GeneralProposalParameters, VoteKind, DiscussionThread, DiscussionPost, CreateOpeningParameters, FillOpeningParameters, TerminateRoleParameters, ProposalDecision, ExecutionFailed, Approved, SetLeadParams, ThreadMode, ExecutionStatus, FundingRequestParameters, ReferendumStageVoting, ReferendumStageRevealing, ReferendumStage, OptionResult, VotePower, ConstitutionInfo, ParticipantId, Title, UpdatedTitle, UpdatedBody, ReplyId, Reply, ReplyToDelete, BountyId, JSBounty, BountyMilestone, EntryId, BountyActor, AssuranceContractType, FundingType_Limited, FundingType_Perpetual, FundingType, BountyCreationParameters, OracleJudgment_Winner, OracleWorkEntryJudgment, OracleJudgment, Entry, CuratorId, CuratorGroupId, CuratorGroup, ContentActor, NewAsset, Channel, ChannelOwner, ChannelCategoryId, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelUpdateParameters, ChannelOwnershipTransferRequestId, ChannelOwnershipTransferRequest, Video, VideoId, VideoCategoryId, VideoCategory, VideoCategoryCreationParameters, VideoCategoryUpdateParameters, VideoCreationParameters, VideoUpdateParameters, Person, PersonId, PersonController, PersonActor, PersonCreationParameters, PersonUpdateParameters, Playlist, PlaylistId, PlaylistCreationParameters, PlaylistUpdateParameters, SeriesId, Series, Season, SeriesParameters, SeasonParameters, EpisodeParemters, MaxNumber, IsCensored }; \ No newline at end of file diff --git a/types/augment-codec/augment-api-query.ts b/types/augment-codec/augment-api-query.ts index 53b08a7568..34fd6b27b7 100644 --- a/types/augment-codec/augment-api-query.ts +++ b/types/augment-codec/augment-api-query.ts @@ -3,7 +3,7 @@ import type { Bytes, Option, Vec, bool, u16, u32, u64, u8 } from '@polkadot/types'; import type { AnyNumber, ITuple, Observable } from '@polkadot/types/types'; -import type { Application, ApplicationId, BountyActor, BountyId, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryId, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ConstitutionInfo, ContentId, CouncilMemberOf, CouncilStageUpdate, CuratorGroup, CuratorGroupId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DiscussionPost, DiscussionThread, Entry, EntryId, ForumUserId, MemberId, Membership, MemoText, ModeratorId, ObjectOwner, Opening, OpeningId, Person, PersonId, Playlist, PlaylistId, Post, PostId, ProposalId, ProposalOf, ReferendumStage, Reply, ReplyId, Series, SeriesId, StakingAccountMemberBinding, ThreadId, ThreadOf, Video, VideoCategory, VideoCategoryId, VideoId, VoteKind, Voucher, Worker, WorkerId } from './all'; +import type { Application, ApplicationId, BountyActor, BountyId, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryId, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ConstitutionInfo, ContentId, CouncilMemberOf, CouncilStageUpdate, CuratorGroup, CuratorGroupId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DiscussionPost, DiscussionThread, Entry, EntryId, ForumUserId, JSBounty as Bounty, MemberId, Membership, MemoText, ModeratorId, ObjectOwner, Opening, OpeningId, Person, PersonId, Playlist, PlaylistId, Post, PostId, ProposalId, ProposalOf, ReferendumStage, Reply, ReplyId, Series, SeriesId, StakingAccountMemberBinding, ThreadId, ThreadOf, Video, VideoCategory, VideoCategoryId, VideoId, VoteKind, Voucher, Worker, WorkerId } from './all'; import type { UncleEntryItem } from '@polkadot/types/interfaces/authorship'; import type { BabeAuthorityWeight, MaybeRandomness, NextConfigDescriptor, Randomness } from '@polkadot/types/interfaces/babe'; import type { AccountData, BalanceLock } from '@polkadot/types/interfaces/balances'; @@ -15,7 +15,6 @@ import type { AccountId, Balance, BalanceOf, BlockNumber, ExtrinsicsWeight, Hash import type { Keys, SessionIndex } from '@polkadot/types/interfaces/session'; import type { ActiveEraInfo, ElectionResult, ElectionScore, ElectionStatus, EraIndex, EraRewardPoints, Exposure, Forcing, Nominations, RewardDestination, SlashingSpans, SpanIndex, SpanRecord, StakingLedger, UnappliedSlash, ValidatorPrefs } from '@polkadot/types/interfaces/staking'; import type { AccountInfo, DigestOf, EventIndex, EventRecord, LastRuntimeUpgradeInfo, Phase } from '@polkadot/types/interfaces/system'; -import type { Bounty } from '@polkadot/types/interfaces/treasury'; import type { Multiplier } from '@polkadot/types/interfaces/txpayment'; import type { ApiTypes } from '@polkadot/api/types'; diff --git a/types/augment-codec/augment-api-tx.ts b/types/augment-codec/augment-api-tx.ts index 8b70751d53..e2a63ebc7f 100644 --- a/types/augment-codec/augment-api-tx.ts +++ b/types/augment-codec/augment-api-tx.ts @@ -273,7 +273,7 @@ declare module '@polkadot/api/types/submittable' { * - `O(N)` * # **/ - submitOracleJudgment: AugmentedSubmittable<(oracle: BountyActor | { Council: any } | { Member: any } | string | Uint8Array, bountyId: BountyId | AnyNumber | Uint8Array, judgment: OracleJudgment | { Winner: any } | { Rejected: any } | string | Uint8Array) => SubmittableExtrinsic, [BountyActor, BountyId, OracleJudgment]>; + submitOracleJudgment: AugmentedSubmittable<(oracle: BountyActor | { Council: any } | { Member: any } | string | Uint8Array, bountyId: BountyId | AnyNumber | Uint8Array, judgment: OracleJudgment) => SubmittableExtrinsic, [BountyActor, BountyId, OracleJudgment]>; /** * Submit work for a bounty. * # diff --git a/types/augment-codec/augment-types.ts b/types/augment-codec/augment-types.ts index b8ba57bb8f..c334c20cc0 100644 --- a/types/augment-codec/augment-types.ts +++ b/types/augment-codec/augment-types.ts @@ -2,7 +2,7 @@ /* eslint-disable */ import type { BitVec, Bool, Bytes, Compact, Data, I128, I16, I256, I32, I64, I8, Json, Null, Option, Raw, StorageKey, Text, Type, U128, U16, U256, U32, U64, U8, USize, Vec, bool, i128, i16, i256, i32, i64, i8, u128, u16, u256, u32, u64, u8, usize } from '@polkadot/types'; -import type { ActorId, Application, ApplicationId, ApplicationIdSet, ApplicationIdToWorkerIdMap, ApplicationInfo, ApplyOnOpeningParameters, Approved, AssuranceContractType, BalanceKind, BlockAndTime, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwner, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ConstitutionInfo, ContentActor, ContentId, ContentParameters, CouncilMemberOf, CouncilStage, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CreateOpeningParameters, CuratorGroup, CuratorGroupId, CuratorId, DAOId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DataObjectsMap, DiscussionPost, DiscussionThread, Entry, EntryId, EpisodeParemters, ExecutionFailed, ExecutionStatus, ExtendedPostId, FillOpeningParameters, ForumUserId, FundingRequestParameters, FundingType, FundingType_Limited, FundingType_Perpetual, GeneralProposalParameters, InputValidationLengthConstraint, InviteMembershipParameters, IsCensored, LiaisonJudgement, MaxNumber, MemberId, Membership, MemoText, ModeratorId, NewAsset, ObjectOwner, Opening, OpeningId, OpeningType, OptionResult, OracleJudgment, OracleJudgment_Winner, ParticipantId, Penalty, Person, PersonActor, PersonController, PersonCreationParameters, PersonId, PersonUpdateParameters, Playlist, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PollAlternative, PollInput, Post, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetails, ProposalDetailsOf, ProposalId, ProposalOf, ProposalParameters, ProposalStatus, ReferendumStage, ReferendumStageRevealing, ReferendumStageVoting, Reply, ReplyId, ReplyToDelete, RewardPaymentType, Season, SeasonParameters, Series, SeriesId, SeriesParameters, SetLeadParams, StakeParameters, StakePolicy, StakingAccountMemberBinding, StorageObjectOwner, StorageProviderId, TerminateRoleParameters, Thread, ThreadId, ThreadMode, ThreadOf, Title, UpdatedBody, UpdatedTitle, UploadingStatus, Url, Video, VideoCategory, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VotePower, VotingResults, Voucher, VoucherLimit, Worker, WorkerId, WorkerInfo, WorkingGroup } from './all'; +import type { ActorId, Application, ApplicationId, ApplicationIdSet, ApplicationIdToWorkerIdMap, ApplicationInfo, ApplyOnOpeningParameters, Approved, AssuranceContractType, BalanceKind, BlockAndTime, BountyActor, BountyCreationParameters, BountyId, BountyMilestone, BuyMembershipParameters, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwner, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ConstitutionInfo, ContentActor, ContentId, ContentParameters, CouncilMemberOf, CouncilStage, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CreateOpeningParameters, CuratorGroup, CuratorGroupId, CuratorId, DAOId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DataObjectsMap, DiscussionPost, DiscussionThread, Entry, EntryId, EpisodeParemters, ExecutionFailed, ExecutionStatus, ExtendedPostId, FillOpeningParameters, ForumUserId, FundingRequestParameters, FundingType, FundingType_Limited, FundingType_Perpetual, GeneralProposalParameters, InputValidationLengthConstraint, InviteMembershipParameters, IsCensored, JSBounty, LiaisonJudgement, MaxNumber, MemberId, Membership, MemoText, ModeratorId, NewAsset, ObjectOwner, Opening, OpeningId, OpeningType, OptionResult, OracleJudgment, OracleJudgment_Winner, OracleWorkEntryJudgment, ParticipantId, Penalty, Person, PersonActor, PersonController, PersonCreationParameters, PersonId, PersonUpdateParameters, Playlist, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PollAlternative, PollInput, Post, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetails, ProposalDetailsOf, ProposalId, ProposalOf, ProposalParameters, ProposalStatus, ReferendumStage, ReferendumStageRevealing, ReferendumStageVoting, Reply, ReplyId, ReplyToDelete, RewardPaymentType, Season, SeasonParameters, Series, SeriesId, SeriesParameters, SetLeadParams, StakeParameters, StakePolicy, StakingAccountMemberBinding, StorageObjectOwner, StorageProviderId, TerminateRoleParameters, Thread, ThreadId, ThreadMode, ThreadOf, Title, UpdatedBody, UpdatedTitle, UploadingStatus, Url, Video, VideoCategory, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VotePower, VotingResults, Voucher, VoucherLimit, Worker, WorkerId, WorkerInfo, WorkingGroup } from './all'; import type { AssetApproval, AssetApprovalKey, AssetBalance, AssetDestroyWitness, AssetDetails, AssetMetadata, TAssetBalance, TAssetDepositBalance } from '@polkadot/types/interfaces/assets'; import type { BlockAttestations, IncludedBlocks, MoreAttestations } from '@polkadot/types/interfaces/attestations'; import type { RawAuraPreDigest } from '@polkadot/types/interfaces/aura'; @@ -311,6 +311,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -682,6 +683,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -802,6 +804,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -1403,6 +1406,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -1774,6 +1778,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -1894,6 +1899,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -2495,6 +2501,7 @@ declare module '@polkadot/types/types/registry' { BountyCreationParameters: BountyCreationParameters; BountyId: BountyId; BountyIndex: BountyIndex; + BountyMilestone: BountyMilestone; BountyStatus: BountyStatus; BountyStatusActive: BountyStatusActive; BountyStatusCuratorProposed: BountyStatusCuratorProposed; @@ -2866,6 +2873,7 @@ declare module '@polkadot/types/types/registry' { InvalidTransaction: InvalidTransaction; InviteMembershipParameters: InviteMembershipParameters; IsCensored: IsCensored; + JSBounty: JSBounty; Json: Json; Junction: Junction; Justification: Justification; @@ -2986,6 +2994,7 @@ declare module '@polkadot/types/types/registry' { OptionResult: OptionResult; OracleJudgment_Winner: OracleJudgment_Winner; OracleJudgment: OracleJudgment; + OracleWorkEntryJudgment: OracleWorkEntryJudgment; Origin: Origin; OriginCaller: OriginCaller; OutboundHrmpMessage: OutboundHrmpMessage; diff --git a/types/augment/all/defs.json b/types/augment/all/defs.json index 7ab9d07070..13518c5161 100644 --- a/types/augment/all/defs.json +++ b/types/augment/all/defs.json @@ -500,6 +500,20 @@ "text_hash": "Hash" }, "BountyId": "u32", + "JSBounty": { + "creation_params": "BountyCreationParameters", + "total_funding": "u128", + "milestone": "BountyMilestone", + "active_work_entry_count": "u32" + }, + "BountyMilestone": { + "_enum": { + "Created": "{\"created_at\":\"u32\",\"has_contributions\":\"bool\"}", + "BountyMaxFundingReached": "{\"max_funding_reached_at\":\"u32\"}", + "WorkSubmitted": "{\"work_period_started_at\":\"u32\"}", + "JudgmentSubmitted": "{\"successful_bounty\":\"bool\"}" + } + }, "EntryId": "u32", "BountyActor": { "_enum": { @@ -540,12 +554,13 @@ "OracleJudgment_Winner": { "reward": "u128" }, - "OracleJudgment": { + "OracleWorkEntryJudgment": { "_enum": { "Winner": "OracleJudgment_Winner", "Rejected": "Null" } }, + "OracleJudgment": "BTreeMap", "Entry": { "member_id": "MemberId", "staking_account_id": "AccountId", diff --git a/types/augment/all/types.ts b/types/augment/all/types.ts index 64a0123a36..f9ec7b0837 100644 --- a/types/augment/all/types.ts +++ b/types/augment/all/types.ts @@ -97,6 +97,27 @@ export interface BountyCreationParameters extends Struct { /** @name BountyId */ export interface BountyId extends u32 {} +/** @name BountyMilestone */ +export interface BountyMilestone extends Enum { + readonly isCreated: boolean; + readonly asCreated: { + readonly created_at: u32; + readonly has_contributions: bool; + } & Struct; + readonly isBountyMaxFundingReached: boolean; + readonly asBountyMaxFundingReached: { + readonly max_funding_reached_at: u32; + } & Struct; + readonly isWorkSubmitted: boolean; + readonly asWorkSubmitted: { + readonly work_period_started_at: u32; + } & Struct; + readonly isJudgmentSubmitted: boolean; + readonly asJudgmentSubmitted: { + readonly successful_bounty: bool; + } & Struct; +} + /** @name BuyMembershipParameters */ export interface BuyMembershipParameters extends Struct { readonly root_account: AccountId; @@ -431,6 +452,14 @@ export interface InviteMembershipParameters extends Struct { /** @name IsCensored */ export interface IsCensored extends bool {} +/** @name JSBounty */ +export interface JSBounty extends Struct { + readonly creation_params: BountyCreationParameters; + readonly total_funding: u128; + readonly milestone: BountyMilestone; + readonly active_work_entry_count: u32; +} + /** @name LiaisonJudgement */ export interface LiaisonJudgement extends Enum { readonly isPending: boolean; @@ -508,17 +537,20 @@ export interface OptionResult extends Struct { } /** @name OracleJudgment */ -export interface OracleJudgment extends Enum { - readonly isWinner: boolean; - readonly asWinner: OracleJudgment_Winner; - readonly isRejected: boolean; -} +export interface OracleJudgment extends BTreeMap {} /** @name OracleJudgment_Winner */ export interface OracleJudgment_Winner extends Struct { readonly reward: u128; } +/** @name OracleWorkEntryJudgment */ +export interface OracleWorkEntryJudgment extends Enum { + readonly isWinner: boolean; + readonly asWinner: OracleJudgment_Winner; + readonly isRejected: boolean; +} + /** @name ParticipantId */ export interface ParticipantId extends u64 {} diff --git a/types/augment/augment-api-tx.ts b/types/augment/augment-api-tx.ts index 8b70751d53..e2a63ebc7f 100644 --- a/types/augment/augment-api-tx.ts +++ b/types/augment/augment-api-tx.ts @@ -273,7 +273,7 @@ declare module '@polkadot/api/types/submittable' { * - `O(N)` * # **/ - submitOracleJudgment: AugmentedSubmittable<(oracle: BountyActor | { Council: any } | { Member: any } | string | Uint8Array, bountyId: BountyId | AnyNumber | Uint8Array, judgment: OracleJudgment | { Winner: any } | { Rejected: any } | string | Uint8Array) => SubmittableExtrinsic, [BountyActor, BountyId, OracleJudgment]>; + submitOracleJudgment: AugmentedSubmittable<(oracle: BountyActor | { Council: any } | { Member: any } | string | Uint8Array, bountyId: BountyId | AnyNumber | Uint8Array, judgment: OracleJudgment) => SubmittableExtrinsic, [BountyActor, BountyId, OracleJudgment]>; /** * Submit work for a bounty. * # diff --git a/types/augment/augment-types.ts b/types/augment/augment-types.ts index b8ba57bb8f..c334c20cc0 100644 --- a/types/augment/augment-types.ts +++ b/types/augment/augment-types.ts @@ -2,7 +2,7 @@ /* eslint-disable */ import type { BitVec, Bool, Bytes, Compact, Data, I128, I16, I256, I32, I64, I8, Json, Null, Option, Raw, StorageKey, Text, Type, U128, U16, U256, U32, U64, U8, USize, Vec, bool, i128, i16, i256, i32, i64, i8, u128, u16, u256, u32, u64, u8, usize } from '@polkadot/types'; -import type { ActorId, Application, ApplicationId, ApplicationIdSet, ApplicationIdToWorkerIdMap, ApplicationInfo, ApplyOnOpeningParameters, Approved, AssuranceContractType, BalanceKind, BlockAndTime, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwner, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ConstitutionInfo, ContentActor, ContentId, ContentParameters, CouncilMemberOf, CouncilStage, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CreateOpeningParameters, CuratorGroup, CuratorGroupId, CuratorId, DAOId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DataObjectsMap, DiscussionPost, DiscussionThread, Entry, EntryId, EpisodeParemters, ExecutionFailed, ExecutionStatus, ExtendedPostId, FillOpeningParameters, ForumUserId, FundingRequestParameters, FundingType, FundingType_Limited, FundingType_Perpetual, GeneralProposalParameters, InputValidationLengthConstraint, InviteMembershipParameters, IsCensored, LiaisonJudgement, MaxNumber, MemberId, Membership, MemoText, ModeratorId, NewAsset, ObjectOwner, Opening, OpeningId, OpeningType, OptionResult, OracleJudgment, OracleJudgment_Winner, ParticipantId, Penalty, Person, PersonActor, PersonController, PersonCreationParameters, PersonId, PersonUpdateParameters, Playlist, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PollAlternative, PollInput, Post, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetails, ProposalDetailsOf, ProposalId, ProposalOf, ProposalParameters, ProposalStatus, ReferendumStage, ReferendumStageRevealing, ReferendumStageVoting, Reply, ReplyId, ReplyToDelete, RewardPaymentType, Season, SeasonParameters, Series, SeriesId, SeriesParameters, SetLeadParams, StakeParameters, StakePolicy, StakingAccountMemberBinding, StorageObjectOwner, StorageProviderId, TerminateRoleParameters, Thread, ThreadId, ThreadMode, ThreadOf, Title, UpdatedBody, UpdatedTitle, UploadingStatus, Url, Video, VideoCategory, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VotePower, VotingResults, Voucher, VoucherLimit, Worker, WorkerId, WorkerInfo, WorkingGroup } from './all'; +import type { ActorId, Application, ApplicationId, ApplicationIdSet, ApplicationIdToWorkerIdMap, ApplicationInfo, ApplyOnOpeningParameters, Approved, AssuranceContractType, BalanceKind, BlockAndTime, BountyActor, BountyCreationParameters, BountyId, BountyMilestone, BuyMembershipParameters, Candidate, CastVoteOf, Category, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwner, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ConstitutionInfo, ContentActor, ContentId, ContentParameters, CouncilMemberOf, CouncilStage, CouncilStageAnnouncing, CouncilStageElection, CouncilStageUpdate, CreateOpeningParameters, CuratorGroup, CuratorGroupId, CuratorId, DAOId, DataObject, DataObjectStorageRelationship, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, DataObjectsMap, DiscussionPost, DiscussionThread, Entry, EntryId, EpisodeParemters, ExecutionFailed, ExecutionStatus, ExtendedPostId, FillOpeningParameters, ForumUserId, FundingRequestParameters, FundingType, FundingType_Limited, FundingType_Perpetual, GeneralProposalParameters, InputValidationLengthConstraint, InviteMembershipParameters, IsCensored, JSBounty, LiaisonJudgement, MaxNumber, MemberId, Membership, MemoText, ModeratorId, NewAsset, ObjectOwner, Opening, OpeningId, OpeningType, OptionResult, OracleJudgment, OracleJudgment_Winner, OracleWorkEntryJudgment, ParticipantId, Penalty, Person, PersonActor, PersonController, PersonCreationParameters, PersonId, PersonUpdateParameters, Playlist, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PollAlternative, PollInput, Post, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetails, ProposalDetailsOf, ProposalId, ProposalOf, ProposalParameters, ProposalStatus, ReferendumStage, ReferendumStageRevealing, ReferendumStageVoting, Reply, ReplyId, ReplyToDelete, RewardPaymentType, Season, SeasonParameters, Series, SeriesId, SeriesParameters, SetLeadParams, StakeParameters, StakePolicy, StakingAccountMemberBinding, StorageObjectOwner, StorageProviderId, TerminateRoleParameters, Thread, ThreadId, ThreadMode, ThreadOf, Title, UpdatedBody, UpdatedTitle, UploadingStatus, Url, Video, VideoCategory, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VotePower, VotingResults, Voucher, VoucherLimit, Worker, WorkerId, WorkerInfo, WorkingGroup } from './all'; import type { AssetApproval, AssetApprovalKey, AssetBalance, AssetDestroyWitness, AssetDetails, AssetMetadata, TAssetBalance, TAssetDepositBalance } from '@polkadot/types/interfaces/assets'; import type { BlockAttestations, IncludedBlocks, MoreAttestations } from '@polkadot/types/interfaces/attestations'; import type { RawAuraPreDigest } from '@polkadot/types/interfaces/aura'; @@ -311,6 +311,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -682,6 +683,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -802,6 +804,7 @@ declare module '@polkadot/types/types/registry' { 'Option': Option; 'Option': Option; 'Option': Option; + 'Option': Option; 'Option': Option; 'Option': Option; 'Option': Option; @@ -1403,6 +1406,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -1774,6 +1778,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -1894,6 +1899,7 @@ declare module '@polkadot/types/types/registry' { 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; + 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; 'Vec': Vec; @@ -2495,6 +2501,7 @@ declare module '@polkadot/types/types/registry' { BountyCreationParameters: BountyCreationParameters; BountyId: BountyId; BountyIndex: BountyIndex; + BountyMilestone: BountyMilestone; BountyStatus: BountyStatus; BountyStatusActive: BountyStatusActive; BountyStatusCuratorProposed: BountyStatusCuratorProposed; @@ -2866,6 +2873,7 @@ declare module '@polkadot/types/types/registry' { InvalidTransaction: InvalidTransaction; InviteMembershipParameters: InviteMembershipParameters; IsCensored: IsCensored; + JSBounty: JSBounty; Json: Json; Junction: Junction; Justification: Justification; @@ -2986,6 +2994,7 @@ declare module '@polkadot/types/types/registry' { OptionResult: OptionResult; OracleJudgment_Winner: OracleJudgment_Winner; OracleJudgment: OracleJudgment; + OracleWorkEntryJudgment: OracleWorkEntryJudgment; Origin: Origin; OriginCaller: OriginCaller; OutboundHrmpMessage: OutboundHrmpMessage; diff --git a/types/src/bounty.ts b/types/src/bounty.ts index 7975b3f421..69202a457d 100644 --- a/types/src/bounty.ts +++ b/types/src/bounty.ts @@ -1,4 +1,4 @@ -import { Null, u32, u128, bool, Option, BTreeSet } from '@polkadot/types' +import { Null, u32, u128, bool, Option, BTreeSet, BTreeMap } from '@polkadot/types' import { JoyEnum, JoyStructDecorated, MemberId, AccountId } from './common' export class BountyId extends u32 {} @@ -44,11 +44,12 @@ export class OracleJudgment_Winner extends JoyStructDecorated({ reward: u128, // Balance }) {} -export class OracleJudgment extends JoyEnum({ +export class OracleWorkEntryJudgment extends JoyEnum({ Winner: OracleJudgment_Winner, Rejected: Null, }) {} +export class OracleJudgment extends BTreeMap.with(EntryId, OracleWorkEntryJudgment) {} export class Entry extends JoyStructDecorated({ member_id: MemberId, staking_account_id: AccountId, @@ -57,8 +58,41 @@ export class Entry extends JoyStructDecorated({ oracle_judgment_result: Option.with(OracleJudgment), }) {} +export class BountyMilestone_Created extends JoyStructDecorated({ + created_at: u32, // BlockNumber + has_contributions: bool, +}) {} + +export class BountyMilestone_BountyMaxFundingReached extends JoyStructDecorated({ + max_funding_reached_at: u32, // BlockNumber +}) {} + +export class BountyMilestone_WorkSubmitted extends JoyStructDecorated({ + work_period_started_at: u32, // BlockNumber +}) {} + +export class BountyMilestone_JudgmentSubmitted extends JoyStructDecorated({ + successful_bounty: bool, +}) {} + +export class BountyMilestone extends JoyEnum({ + Created: BountyMilestone_Created, + BountyMaxFundingReached: BountyMilestone_BountyMaxFundingReached, + WorkSubmitted: BountyMilestone_WorkSubmitted, + JudgmentSubmitted: BountyMilestone_JudgmentSubmitted, +}) {} + +export class JSBounty extends JoyStructDecorated({ + creation_params: BountyCreationParameters, + total_funding: u128, + milestone: BountyMilestone, + active_work_entry_count: u32, +}) {} + export const bountyTypes = { BountyId, + JSBounty, + BountyMilestone, EntryId, BountyActor, AssuranceContractType, @@ -67,6 +101,7 @@ export const bountyTypes = { FundingType, BountyCreationParameters, OracleJudgment_Winner, + OracleWorkEntryJudgment, OracleJudgment, Entry, }