Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bounty module CLI commands #3102

Open
wants to merge 16 commits into
base: rhodes
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
207 changes: 207 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions cli/examples/bounty/CreateBounty.json
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
30 changes: 28 additions & 2 deletions cli/src/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, any>) {
Expand Down Expand Up @@ -264,7 +265,7 @@ export default class Api {
return membership.isEmpty ? null : await this.memberDetails(memberId, membership)
}

protected async expectedMembershipById(memberId: MemberId): Promise<MemberDetails> {
async expectedMembershipById(memberId: MemberId): Promise<MemberDetails> {
const member = await this.membershipById(memberId)
if (!member) {
throw new CLIError(`Expected member was not found by id: ${memberId.toString()}`)
Expand Down Expand Up @@ -458,6 +459,31 @@ export default class Api {
return this.entriesByIds<MemberId, Membership>(this._api.query.members.membershipById)
}

// Bounty
async availableBounties(): Promise<[BountyId, Bounty][]> {
return await this.entriesByIds<BountyId, Bounty>(this._api.query.bounty.bounties)
}

async bountyById(bountyId: BountyId | number | string): Promise<Bounty> {
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<Entry> {
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<EntryId, Entry>(this._api.query.bounty.entries)
}

// Content directory
async availableChannels(): Promise<[ChannelId, Channel][]> {
return await this.entriesByIds<ChannelId, Channel>(this._api.query.content.channelById)
Expand Down
40 changes: 40 additions & 0 deletions cli/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
IVideoMetadata,
IVideoCategoryMetadata,
IChannelCategoryMetadata,
IBountyMetadata,
IBountyWorkData,
} from '@joystream/metadata-protobuf'

// KeyringPair type extended with mandatory "meta.name"
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion cli/src/base/AccountsCommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 })
}

Expand Down Expand Up @@ -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<CouncilMemberOf> {
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<MemberDetails> {
// TODO: Limit only to a set of members provided by the user?
const allMembers = await this.getApi().allMembers()
Expand Down