瀏覽代碼

CLI - Query-node integration (membership handles)

Leszek Wiesner 4 年之前
父節點
當前提交
2b81c0504b

+ 12 - 0
cli/codegen.yml

@@ -0,0 +1,12 @@
+overwrite: true
+
+schema: '../query-node/generated/graphql-server/generated/schema.graphql'
+
+generates:
+  src/QueryNodeApiSchema.generated.ts:
+    hooks:
+      afterOneFileWrite:
+        - prettier --write
+        - eslint --fix
+    plugins:
+      - typescript

+ 5 - 1
cli/package.json

@@ -8,6 +8,8 @@
   },
   "bugs": "https://github.com/Joystream/joystream/issues",
   "dependencies": {
+    "@apollo/client": "^3.3.13",
+    "cross-fetch": "^3.0.6",
     "@apidevtools/json-schema-ref-parser": "^9.0.6",
     "@ffprobe-installer/ffprobe": "^1.1.0",
     "@joystream/types": "^0.15.0",
@@ -124,7 +126,9 @@
     "lint": "eslint ./src --ext .ts",
     "checks": "tsc --noEmit --pretty && prettier ./ --check && yarn lint",
     "format": "prettier ./ --write",
-    "generate:schema-typings": "rm -rf ./src/json-schemas/typings && json2ts -i ./src/json-schemas/ -o ./src/json-schemas/typings/"
+    "generate:schema-typings": "rm -rf ./src/json-schemas/typings && json2ts -i ./src/json-schemas/ -o ./src/json-schemas/typings/",
+    "generate:graphql-typings": "graphql-codegen",
+    "generate:all": "yarn generate:schema-typings && yarn generate:graphql-typings"
   },
   "types": "lib/index.d.ts",
   "volta": {

+ 73 - 7
cli/src/Api.ts

@@ -15,6 +15,8 @@ import {
   ApplicationDetails,
   OpeningDetails,
   UnaugmentedApiPromise,
+  MemberDetails,
+  GraphQLQueryResult,
 } from './Types'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
@@ -33,6 +35,8 @@ import { Class, ClassId, CuratorGroup, CuratorGroupId, Entity, EntityId } from '
 import { ContentId, DataObject } from '@joystream/types/media'
 import { ServiceProviderRecord } from '@joystream/types/discovery'
 import _ from 'lodash'
+import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject, gql } from '@apollo/client'
+import fetch from 'cross-fetch'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
 const DEFAULT_DECIMALS = new BN(12)
@@ -48,12 +52,18 @@ export const apiModuleByGroup = {
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
   private _api: ApiPromise
+  private _queryNode?: ApolloClient<NormalizedCacheObject>
   private _cdClassesCache: [ClassId, Class][] | null = null
   public isDevelopment = false
 
-  private constructor(originalApi: ApiPromise, isDevelopment: boolean) {
+  private constructor(
+    originalApi: ApiPromise,
+    isDevelopment: boolean,
+    queryNodeClient?: ApolloClient<NormalizedCacheObject>
+  ) {
     this.isDevelopment = isDevelopment
     this._api = originalApi
+    this._queryNode = queryNodeClient
   }
 
   public getOriginalApi(): ApiPromise {
@@ -84,9 +94,22 @@ export default class Api {
     return { api, properties, chainType }
   }
 
-  static async create(apiUri: string = DEFAULT_API_URI, metadataCache: Record<string, any>): Promise<Api> {
+  private static async createQueryNodeClient(uri: string) {
+    return new ApolloClient({
+      link: new HttpLink({ uri, fetch }),
+      cache: new InMemoryCache(),
+      defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
+    })
+  }
+
+  static async create(
+    apiUri = DEFAULT_API_URI,
+    metadataCache: Record<string, any>,
+    queryNodeUri?: string
+  ): Promise<Api> {
     const { api, chainType } = await Api.initApi(apiUri, metadataCache)
-    return new Api(api, chainType.isDevelopment || chainType.isLocal)
+    const queryNodeClient = queryNodeUri ? await this.createQueryNodeClient(queryNodeUri) : undefined
+    return new Api(api, chainType.isDevelopment || chainType.isLocal, queryNodeClient)
   }
 
   async bestNumber(): Promise<number> {
@@ -150,10 +173,53 @@ export default class Api {
     return this._api.query[module]
   }
 
-  protected async membershipById(memberId: MemberId): Promise<Membership | null> {
-    const profile = await this._api.query.members.membershipById(memberId)
+  protected async fetchMemberQueryNodeData(
+    memberId: MemberId
+  ): Promise<GraphQLQueryResult<'membership', 'handle' | 'name'>['membership']> {
+    const MEMBER_BY_ID_QUERY = gql`
+      query($id: ID!) {
+        membership(where: { id: $id }) {
+          handle
+          name
+        }
+      }
+    `
+
+    if (!this._queryNode) {
+      return null
+    }
+
+    const res = await this._queryNode.query<GraphQLQueryResult<'membership', 'handle' | 'name'>>({
+      query: MEMBER_BY_ID_QUERY,
+      variables: { id: memberId.toNumber() },
+    })
+
+    return res.data.membership
+  }
+
+  async memberDetails(memberId: MemberId, membership: Membership): Promise<MemberDetails> {
+    const memberData = await this.fetchMemberQueryNodeData(memberId)
+
+    return {
+      id: memberId,
+      name: memberData?.name,
+      handle: memberData?.handle,
+      membership,
+    }
+  }
+
+  protected async membershipById(memberId: MemberId): Promise<MemberDetails | null> {
+    const membership = await this._api.query.members.membershipById(memberId)
+    return membership.isEmpty ? null : await this.memberDetails(memberId, membership)
+  }
+
+  protected 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()}`)
+    }
 
-    return profile.isEmpty ? null : profile
+    return member
   }
 
   async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
@@ -267,7 +333,7 @@ export default class Api {
   ): Promise<ApplicationDetails> {
     return {
       applicationId,
-      member: await this.membershipById(application.member_id),
+      member: await this.expectedMembershipById(application.member_id),
       roleAccout: application.role_account_id,
       rewardAccount: application.reward_account_id,
       stakingAccount: application.staking_account_id,

+ 636 - 0
cli/src/QueryNodeApiSchema.generated.ts

@@ -0,0 +1,636 @@
+export type Maybe<T> = T | null
+export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
+export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }
+export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+  ID: string
+  String: string
+  Boolean: boolean
+  Int: number
+  Float: number
+  /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
+  DateTime: any
+  /** GraphQL representation of BigInt */
+  BigInt: any
+}
+
+export type BaseGraphQlObject = {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+}
+
+export type BaseModel = BaseGraphQlObject & {
+  __typename?: 'BaseModel'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+}
+
+export type BaseModelUuid = BaseGraphQlObject & {
+  __typename?: 'BaseModelUUID'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+}
+
+export type BaseWhereInput = {
+  id_eq?: Maybe<Scalars['String']>
+  id_in?: Maybe<Array<Scalars['String']>>
+  createdAt_eq?: Maybe<Scalars['String']>
+  createdAt_lt?: Maybe<Scalars['String']>
+  createdAt_lte?: Maybe<Scalars['String']>
+  createdAt_gt?: Maybe<Scalars['String']>
+  createdAt_gte?: Maybe<Scalars['String']>
+  createdById_eq?: Maybe<Scalars['String']>
+  updatedAt_eq?: Maybe<Scalars['String']>
+  updatedAt_lt?: Maybe<Scalars['String']>
+  updatedAt_lte?: Maybe<Scalars['String']>
+  updatedAt_gt?: Maybe<Scalars['String']>
+  updatedAt_gte?: Maybe<Scalars['String']>
+  updatedById_eq?: Maybe<Scalars['String']>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['String']>
+  deletedAt_lt?: Maybe<Scalars['String']>
+  deletedAt_lte?: Maybe<Scalars['String']>
+  deletedAt_gt?: Maybe<Scalars['String']>
+  deletedAt_gte?: Maybe<Scalars['String']>
+  deletedById_eq?: Maybe<Scalars['String']>
+}
+
+export type Block = BaseGraphQlObject & {
+  __typename?: 'Block'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  block: Scalars['Int']
+  executedAt: Scalars['DateTime']
+  network: Network
+  membershipregisteredAtBlock?: Maybe<Array<Membership>>
+}
+
+export type BlockConnection = {
+  __typename?: 'BlockConnection'
+  totalCount: Scalars['Int']
+  edges: Array<BlockEdge>
+  pageInfo: PageInfo
+}
+
+export type BlockCreateInput = {
+  block: Scalars['Float']
+  executedAt: Scalars['DateTime']
+  network: Network
+}
+
+export type BlockEdge = {
+  __typename?: 'BlockEdge'
+  node: Block
+  cursor: Scalars['String']
+}
+
+export enum BlockOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  BlockAsc = 'block_ASC',
+  BlockDesc = 'block_DESC',
+  ExecutedAtAsc = 'executedAt_ASC',
+  ExecutedAtDesc = 'executedAt_DESC',
+  NetworkAsc = 'network_ASC',
+  NetworkDesc = 'network_DESC',
+}
+
+export type BlockUpdateInput = {
+  block?: Maybe<Scalars['Float']>
+  executedAt?: Maybe<Scalars['DateTime']>
+  network?: Maybe<Network>
+}
+
+export type BlockWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  block_eq?: Maybe<Scalars['Int']>
+  block_gt?: Maybe<Scalars['Int']>
+  block_gte?: Maybe<Scalars['Int']>
+  block_lt?: Maybe<Scalars['Int']>
+  block_lte?: Maybe<Scalars['Int']>
+  block_in?: Maybe<Array<Scalars['Int']>>
+  executedAt_eq?: Maybe<Scalars['DateTime']>
+  executedAt_lt?: Maybe<Scalars['DateTime']>
+  executedAt_lte?: Maybe<Scalars['DateTime']>
+  executedAt_gt?: Maybe<Scalars['DateTime']>
+  executedAt_gte?: Maybe<Scalars['DateTime']>
+  network_eq?: Maybe<Network>
+  network_in?: Maybe<Array<Network>>
+}
+
+export type BlockWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export type DeleteResponse = {
+  id: Scalars['ID']
+}
+
+export type MembersByHandleFtsOutput = {
+  __typename?: 'MembersByHandleFTSOutput'
+  item: MembersByHandleSearchResult
+  rank: Scalars['Float']
+  isTypeOf: Scalars['String']
+  highlight: Scalars['String']
+}
+
+export type MembersByHandleSearchResult = Membership
+
+/** Stored information about a registered user */
+export type Membership = BaseGraphQlObject & {
+  __typename?: 'Membership'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** The unique handle chosen by member */
+  handle: Scalars['String']
+  /** Member's name */
+  name?: Maybe<Scalars['String']>
+  /** A Url to member's Avatar image */
+  avatarUri?: Maybe<Scalars['String']>
+  /** Short text chosen by member to share information about themselves */
+  about?: Maybe<Scalars['String']>
+  /** Member's controller account id */
+  controllerAccount: Scalars['String']
+  /** Member's root account id */
+  rootAccount: Scalars['String']
+  registeredAtBlock: Block
+  registeredAtBlockId: Scalars['String']
+  /** Timestamp when member was registered */
+  registeredAtTime: Scalars['DateTime']
+  /** How the member was registered */
+  entry: MembershipEntryMethod
+  /** Whether member has been verified by membership working group. */
+  isVerified: Scalars['Boolean']
+  /** Staking accounts bounded to membership. */
+  boundAccounts: Array<Scalars['String']>
+  /** Current count of invites left to send. */
+  inviteCount: Scalars['Int']
+  invitees: Array<Membership>
+  invitedBy?: Maybe<Membership>
+  invitedById?: Maybe<Scalars['String']>
+  referredMembers: Array<Membership>
+  referredBy?: Maybe<Membership>
+  referredById?: Maybe<Scalars['String']>
+}
+
+export type MembershipConnection = {
+  __typename?: 'MembershipConnection'
+  totalCount: Scalars['Int']
+  edges: Array<MembershipEdge>
+  pageInfo: PageInfo
+}
+
+export type MembershipCreateInput = {
+  handle: Scalars['String']
+  name?: Maybe<Scalars['String']>
+  avatarUri?: Maybe<Scalars['String']>
+  about?: Maybe<Scalars['String']>
+  controllerAccount: Scalars['String']
+  rootAccount: Scalars['String']
+  registeredAtBlockId: Scalars['ID']
+  registeredAtTime: Scalars['DateTime']
+  entry: MembershipEntryMethod
+  isVerified: Scalars['Boolean']
+  boundAccounts: Array<Scalars['String']>
+  inviteCount: Scalars['Float']
+  invitedById?: Maybe<Scalars['ID']>
+  referredById?: Maybe<Scalars['ID']>
+}
+
+export type MembershipEdge = {
+  __typename?: 'MembershipEdge'
+  node: Membership
+  cursor: Scalars['String']
+}
+
+export enum MembershipEntryMethod {
+  Paid = 'PAID',
+  Invited = 'INVITED',
+  Genesis = 'GENESIS',
+}
+
+export enum MembershipOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  HandleAsc = 'handle_ASC',
+  HandleDesc = 'handle_DESC',
+  NameAsc = 'name_ASC',
+  NameDesc = 'name_DESC',
+  AvatarUriAsc = 'avatarUri_ASC',
+  AvatarUriDesc = 'avatarUri_DESC',
+  AboutAsc = 'about_ASC',
+  AboutDesc = 'about_DESC',
+  ControllerAccountAsc = 'controllerAccount_ASC',
+  ControllerAccountDesc = 'controllerAccount_DESC',
+  RootAccountAsc = 'rootAccount_ASC',
+  RootAccountDesc = 'rootAccount_DESC',
+  RegisteredAtBlockIdAsc = 'registeredAtBlockId_ASC',
+  RegisteredAtBlockIdDesc = 'registeredAtBlockId_DESC',
+  RegisteredAtTimeAsc = 'registeredAtTime_ASC',
+  RegisteredAtTimeDesc = 'registeredAtTime_DESC',
+  EntryAsc = 'entry_ASC',
+  EntryDesc = 'entry_DESC',
+  IsVerifiedAsc = 'isVerified_ASC',
+  IsVerifiedDesc = 'isVerified_DESC',
+  InviteCountAsc = 'inviteCount_ASC',
+  InviteCountDesc = 'inviteCount_DESC',
+  InvitedByIdAsc = 'invitedById_ASC',
+  InvitedByIdDesc = 'invitedById_DESC',
+  ReferredByIdAsc = 'referredById_ASC',
+  ReferredByIdDesc = 'referredById_DESC',
+}
+
+export type MembershipSystem = BaseGraphQlObject & {
+  __typename?: 'MembershipSystem'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** Initial invitation count of a new member. */
+  defaultInviteCount: Scalars['Int']
+  /** Current price to buy a membership. */
+  membershipPrice: Scalars['BigInt']
+  /** Amount of tokens diverted to invitor. */
+  referralCut: Scalars['BigInt']
+  /** The initial, locked, balance credited to controller account of invitee. */
+  invitedInitialBalance: Scalars['BigInt']
+}
+
+export type MembershipSystemConnection = {
+  __typename?: 'MembershipSystemConnection'
+  totalCount: Scalars['Int']
+  edges: Array<MembershipSystemEdge>
+  pageInfo: PageInfo
+}
+
+export type MembershipSystemCreateInput = {
+  defaultInviteCount: Scalars['Float']
+  membershipPrice: Scalars['BigInt']
+  referralCut: Scalars['BigInt']
+  invitedInitialBalance: Scalars['BigInt']
+}
+
+export type MembershipSystemEdge = {
+  __typename?: 'MembershipSystemEdge'
+  node: MembershipSystem
+  cursor: Scalars['String']
+}
+
+export enum MembershipSystemOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  DefaultInviteCountAsc = 'defaultInviteCount_ASC',
+  DefaultInviteCountDesc = 'defaultInviteCount_DESC',
+  MembershipPriceAsc = 'membershipPrice_ASC',
+  MembershipPriceDesc = 'membershipPrice_DESC',
+  ReferralCutAsc = 'referralCut_ASC',
+  ReferralCutDesc = 'referralCut_DESC',
+  InvitedInitialBalanceAsc = 'invitedInitialBalance_ASC',
+  InvitedInitialBalanceDesc = 'invitedInitialBalance_DESC',
+}
+
+export type MembershipSystemUpdateInput = {
+  defaultInviteCount?: Maybe<Scalars['Float']>
+  membershipPrice?: Maybe<Scalars['BigInt']>
+  referralCut?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance?: Maybe<Scalars['BigInt']>
+}
+
+export type MembershipSystemWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  defaultInviteCount_eq?: Maybe<Scalars['Int']>
+  defaultInviteCount_gt?: Maybe<Scalars['Int']>
+  defaultInviteCount_gte?: Maybe<Scalars['Int']>
+  defaultInviteCount_lt?: Maybe<Scalars['Int']>
+  defaultInviteCount_lte?: Maybe<Scalars['Int']>
+  defaultInviteCount_in?: Maybe<Array<Scalars['Int']>>
+  membershipPrice_eq?: Maybe<Scalars['BigInt']>
+  membershipPrice_gt?: Maybe<Scalars['BigInt']>
+  membershipPrice_gte?: Maybe<Scalars['BigInt']>
+  membershipPrice_lt?: Maybe<Scalars['BigInt']>
+  membershipPrice_lte?: Maybe<Scalars['BigInt']>
+  membershipPrice_in?: Maybe<Array<Scalars['BigInt']>>
+  referralCut_eq?: Maybe<Scalars['BigInt']>
+  referralCut_gt?: Maybe<Scalars['BigInt']>
+  referralCut_gte?: Maybe<Scalars['BigInt']>
+  referralCut_lt?: Maybe<Scalars['BigInt']>
+  referralCut_lte?: Maybe<Scalars['BigInt']>
+  referralCut_in?: Maybe<Array<Scalars['BigInt']>>
+  invitedInitialBalance_eq?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance_gt?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance_gte?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance_lt?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance_lte?: Maybe<Scalars['BigInt']>
+  invitedInitialBalance_in?: Maybe<Array<Scalars['BigInt']>>
+}
+
+export type MembershipSystemWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export type MembershipUpdateInput = {
+  handle?: Maybe<Scalars['String']>
+  name?: Maybe<Scalars['String']>
+  avatarUri?: Maybe<Scalars['String']>
+  about?: Maybe<Scalars['String']>
+  controllerAccount?: Maybe<Scalars['String']>
+  rootAccount?: Maybe<Scalars['String']>
+  registeredAtBlockId?: Maybe<Scalars['ID']>
+  registeredAtTime?: Maybe<Scalars['DateTime']>
+  entry?: Maybe<MembershipEntryMethod>
+  isVerified?: Maybe<Scalars['Boolean']>
+  boundAccounts?: Maybe<Array<Scalars['String']>>
+  inviteCount?: Maybe<Scalars['Float']>
+  invitedById?: Maybe<Scalars['ID']>
+  referredById?: Maybe<Scalars['ID']>
+}
+
+export type MembershipWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  handle_eq?: Maybe<Scalars['String']>
+  handle_contains?: Maybe<Scalars['String']>
+  handle_startsWith?: Maybe<Scalars['String']>
+  handle_endsWith?: Maybe<Scalars['String']>
+  handle_in?: Maybe<Array<Scalars['String']>>
+  name_eq?: Maybe<Scalars['String']>
+  name_contains?: Maybe<Scalars['String']>
+  name_startsWith?: Maybe<Scalars['String']>
+  name_endsWith?: Maybe<Scalars['String']>
+  name_in?: Maybe<Array<Scalars['String']>>
+  avatarUri_eq?: Maybe<Scalars['String']>
+  avatarUri_contains?: Maybe<Scalars['String']>
+  avatarUri_startsWith?: Maybe<Scalars['String']>
+  avatarUri_endsWith?: Maybe<Scalars['String']>
+  avatarUri_in?: Maybe<Array<Scalars['String']>>
+  about_eq?: Maybe<Scalars['String']>
+  about_contains?: Maybe<Scalars['String']>
+  about_startsWith?: Maybe<Scalars['String']>
+  about_endsWith?: Maybe<Scalars['String']>
+  about_in?: Maybe<Array<Scalars['String']>>
+  controllerAccount_eq?: Maybe<Scalars['String']>
+  controllerAccount_contains?: Maybe<Scalars['String']>
+  controllerAccount_startsWith?: Maybe<Scalars['String']>
+  controllerAccount_endsWith?: Maybe<Scalars['String']>
+  controllerAccount_in?: Maybe<Array<Scalars['String']>>
+  rootAccount_eq?: Maybe<Scalars['String']>
+  rootAccount_contains?: Maybe<Scalars['String']>
+  rootAccount_startsWith?: Maybe<Scalars['String']>
+  rootAccount_endsWith?: Maybe<Scalars['String']>
+  rootAccount_in?: Maybe<Array<Scalars['String']>>
+  registeredAtBlockId_eq?: Maybe<Scalars['ID']>
+  registeredAtBlockId_in?: Maybe<Array<Scalars['ID']>>
+  registeredAtTime_eq?: Maybe<Scalars['DateTime']>
+  registeredAtTime_lt?: Maybe<Scalars['DateTime']>
+  registeredAtTime_lte?: Maybe<Scalars['DateTime']>
+  registeredAtTime_gt?: Maybe<Scalars['DateTime']>
+  registeredAtTime_gte?: Maybe<Scalars['DateTime']>
+  entry_eq?: Maybe<MembershipEntryMethod>
+  entry_in?: Maybe<Array<MembershipEntryMethod>>
+  isVerified_eq?: Maybe<Scalars['Boolean']>
+  isVerified_in?: Maybe<Array<Scalars['Boolean']>>
+  inviteCount_eq?: Maybe<Scalars['Int']>
+  inviteCount_gt?: Maybe<Scalars['Int']>
+  inviteCount_gte?: Maybe<Scalars['Int']>
+  inviteCount_lt?: Maybe<Scalars['Int']>
+  inviteCount_lte?: Maybe<Scalars['Int']>
+  inviteCount_in?: Maybe<Array<Scalars['Int']>>
+  invitedById_eq?: Maybe<Scalars['ID']>
+  invitedById_in?: Maybe<Array<Scalars['ID']>>
+  referredById_eq?: Maybe<Scalars['ID']>
+  referredById_in?: Maybe<Array<Scalars['ID']>>
+}
+
+export type MembershipWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>
+  handle?: Maybe<Scalars['String']>
+}
+
+export enum Network {
+  Babylon = 'BABYLON',
+  Alexandria = 'ALEXANDRIA',
+  Rome = 'ROME',
+  Olympia = 'OLYMPIA',
+}
+
+export type PageInfo = {
+  __typename?: 'PageInfo'
+  hasNextPage: Scalars['Boolean']
+  hasPreviousPage: Scalars['Boolean']
+  startCursor?: Maybe<Scalars['String']>
+  endCursor?: Maybe<Scalars['String']>
+}
+
+export type ProcessorState = {
+  __typename?: 'ProcessorState'
+  lastCompleteBlock: Scalars['Float']
+  lastProcessedEvent: Scalars['String']
+  indexerHead: Scalars['Float']
+  chainHead: Scalars['Float']
+}
+
+export type Query = {
+  __typename?: 'Query'
+  blocks: Array<Block>
+  block?: Maybe<Block>
+  blocksConnection: BlockConnection
+  membershipSystems: Array<MembershipSystem>
+  membershipSystem?: Maybe<MembershipSystem>
+  membershipSystemsConnection: MembershipSystemConnection
+  memberships: Array<Membership>
+  membership?: Maybe<Membership>
+  membershipsConnection: MembershipConnection
+  membersByHandle: Array<MembersByHandleFtsOutput>
+}
+
+export type QueryBlocksArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<BlockWhereInput>
+  orderBy?: Maybe<BlockOrderByInput>
+}
+
+export type QueryBlockArgs = {
+  where: BlockWhereUniqueInput
+}
+
+export type QueryBlocksConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<BlockWhereInput>
+  orderBy?: Maybe<BlockOrderByInput>
+}
+
+export type QueryMembershipSystemsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<MembershipSystemWhereInput>
+  orderBy?: Maybe<MembershipSystemOrderByInput>
+}
+
+export type QueryMembershipSystemArgs = {
+  where: MembershipSystemWhereUniqueInput
+}
+
+export type QueryMembershipSystemsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<MembershipSystemWhereInput>
+  orderBy?: Maybe<MembershipSystemOrderByInput>
+}
+
+export type QueryMembershipsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<MembershipWhereInput>
+  orderBy?: Maybe<MembershipOrderByInput>
+}
+
+export type QueryMembershipArgs = {
+  where: MembershipWhereUniqueInput
+}
+
+export type QueryMembershipsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<MembershipWhereInput>
+  orderBy?: Maybe<MembershipOrderByInput>
+}
+
+export type QueryMembersByHandleArgs = {
+  whereMembership?: Maybe<MembershipWhereInput>
+  skip?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  text: Scalars['String']
+}
+
+export type StandardDeleteResponse = {
+  __typename?: 'StandardDeleteResponse'
+  id: Scalars['ID']
+}
+
+export type Subscription = {
+  __typename?: 'Subscription'
+  stateSubscription: ProcessorState
+}

+ 21 - 2
cli/src/Types.ts

@@ -8,6 +8,7 @@ import { MemberId } from '@joystream/types/common'
 import { Validator } from 'inquirer'
 import { ApiPromise } from '@polkadot/api'
 import { SubmittableModuleExtrinsics, QueryableModuleStorage, QueryableModuleConsts } from '@polkadot/api/types'
+import { Query } from './QueryNodeApiSchema.generated'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -54,14 +55,14 @@ export type GroupMember = {
   workerId: WorkerId
   memberId: MemberId
   roleAccount: AccountId
-  profile: Membership
+  profile: MemberDetails
   stake: Balance
   reward: Reward
 }
 
 export type ApplicationDetails = {
   applicationId: number
-  member: Membership | null
+  member: MemberDetails
   roleAccout: AccountId
   stakingAccount: AccountId
   rewardAccount: AccountId
@@ -81,6 +82,14 @@ export type OpeningDetails = {
   rewardPerBlock?: Balance
 }
 
+// Extended membership information (including optional query node data)
+export type MemberDetails = {
+  id: MemberId
+  name?: string | null
+  handle?: string
+  membership: Membership
+}
+
 // Api-related
 
 // Additional options that can be passed to ApiCommandBase.promptForParam in order to override
@@ -111,3 +120,13 @@ export type UnaugmentedApiPromise = Omit<ApiPromise, 'query' | 'tx' | 'consts'>
   tx: { [key: string]: SubmittableModuleExtrinsics<'promise'> }
   consts: { [key: string]: QueryableModuleConsts }
 }
+
+type Maybe<T> = T | null
+
+// Helper for creating partial GraphQL query result type
+export type GraphQLQueryResult<
+  QueryName extends keyof Query,
+  Fields extends keyof NonNullable<Pick<Query, QueryName>[QueryName]>
+> = Pick<Query, QueryName>[QueryName] extends Maybe<Pick<Query, QueryName>[QueryName]>
+  ? { [K in QueryName]?: Maybe<Pick<NonNullable<Pick<Query, QueryName>[QueryName]>, Fields>> }
+  : { [K in QueryName]: Pick<NonNullable<Pick<Query, QueryName>[QueryName]>, Fields> }

+ 11 - 10
cli/src/base/AccountsCommandBase.ts

@@ -6,9 +6,9 @@ import { CLIError } from '@oclif/errors'
 import ApiCommandBase from './ApiCommandBase'
 import { Keyring } from '@polkadot/api'
 import { formatBalance } from '@polkadot/util'
-import { NamedKeyringPair } from '../Types'
+import { MemberDetails, NamedKeyringPair } from '../Types'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
-import { toFixedLength } from '../helpers/display'
+import { memberHandle, toFixedLength } from '../helpers/display'
 import { AccountId, MemberId } from '@joystream/types/common'
 import { KeyringPair, KeyringInstance, KeyringOptions } from '@polkadot/keyring/types'
 import { KeypairType } from '@polkadot/util-crypto/types'
@@ -326,10 +326,14 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
   }
 
-  async getRequiredMemberContext(): Promise<[MemberId, Membership]> {
+  async getRequiredMemberContext(): Promise<MemberDetails> {
     // TODO: Limit only to a set of members provided by the user?
     const allMembers = await this.getApi().allMembers()
-    const availableMembers = allMembers.filter(([, m]) => this.isKeyAvailable(m.controller_account.toString()))
+    const availableMembers = await Promise.all(
+      allMembers
+        .filter(([, m]) => this.isKeyAvailable(m.controller_account.toString()))
+        .map(([id, m]) => this.getApi().memberDetails(id, m))
+    )
 
     if (!availableMembers.length) {
       this.error('No member controller key available!', { exit: ExitCodes.AccessDenied })
@@ -340,15 +344,12 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
   }
 
-  async promptForMember(
-    availableMembers: [MemberId, Membership][],
-    message = 'Choose a member'
-  ): Promise<[MemberId, Membership]> {
+  async promptForMember(availableMembers: MemberDetails[], message = 'Choose a member'): Promise<MemberDetails> {
     const memberIndex = await this.simplePrompt({
       type: 'list',
       message,
-      choices: availableMembers.map(([, m], i) => ({
-        name: m.handle_hash.toString(),
+      choices: availableMembers.map((m, i) => ({
+        name: memberHandle(m),
         value: i,
       })),
     })

+ 62 - 4
cli/src/base/ApiCommandBase.ts

@@ -26,7 +26,6 @@ export class ExtrinsicFailedError extends Error {}
  */
 export default abstract class ApiCommandBase extends StateAwareCommandBase {
   private api: Api | null = null
-  forceSkipApiUriPrompt = false
 
   getApi(): Api {
     if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError })
@@ -53,13 +52,20 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
   async init() {
     await super.init()
     let apiUri: string = this.getPreservedState().apiUri
+
     if (!apiUri) {
-      this.warn("You haven't provided a node/endpoint for the CLI to connect to yet!")
+      this.warn("You haven't provided a Joystream node websocket api uri for the CLI to connect to yet!")
       apiUri = await this.promptForApiUri()
     }
 
+    let queryNodeUri: string = this.getPreservedState().queryNodeUri
+    if (!queryNodeUri) {
+      this.warn("You haven't provided a Joystream query node uri for the CLI to connect to yet!")
+      queryNodeUri = await this.promptForQueryNodeUri()
+    }
+
     const { metadataCache } = this.getPreservedState()
-    this.api = await Api.create(apiUri, metadataCache)
+    this.api = await Api.create(apiUri, metadataCache, queryNodeUri === 'none' ? undefined : queryNodeUri)
 
     const { genesisHash, runtimeVersion } = this.getOriginalApi()
     const metadataKey = `${genesisHash}-${runtimeVersion.specVersion}`
@@ -73,7 +79,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
   async promptForApiUri(): Promise<string> {
     let selectedNodeUri = await this.simplePrompt({
       type: 'list',
-      message: 'Choose a node/endpoint:',
+      message: 'Choose a node websocket api uri:',
       choices: [
         {
           name: 'Local node (ws://localhost:9944)',
@@ -107,6 +113,47 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return selectedNodeUri
   }
 
+  async promptForQueryNodeUri(): Promise<string> {
+    let selectedUri = await this.simplePrompt({
+      type: 'list',
+      message: 'Choose a query node endpoint:',
+      choices: [
+        {
+          name: 'Local query node (http://localhost:8081/graphql)',
+          value: 'http://localhost:8081/graphql',
+        },
+        {
+          name: 'Jsgenesis-hosted query node (https://hydra.joystream.org/graphql)',
+          value: 'https://hydra.joystream.org/graphql',
+        },
+        {
+          name: 'Custom endpoint',
+          value: '',
+        },
+        {
+          name: "No endpoint (if you don't use query node some features will not be available)",
+          value: 'none',
+        },
+      ],
+    })
+
+    if (!selectedUri) {
+      do {
+        selectedUri = await this.simplePrompt({
+          type: 'input',
+          message: 'Provide a query node endpoint',
+        })
+        if (!this.isApiUriValid(selectedUri)) {
+          this.warn('Provided uri seems incorrect! Please try again...')
+        }
+      } while (!this.isApiUriValid(selectedUri))
+    }
+
+    await this.setPreservedState({ queryNodeUri: selectedUri })
+
+    return selectedUri
+  }
+
   isApiUriValid(uri: string) {
     try {
       // eslint-disable-next-line no-new
@@ -117,6 +164,17 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return true
   }
 
+  isQueryNodeUriValid(uri: string) {
+    let url: URL
+    try {
+      url = new URL(uri)
+    } catch (_) {
+      return false
+    }
+
+    return url.protocol === 'http:' || url.protocol === 'https:'
+  }
+
   // This is needed to correctly handle some structs, enums etc.
   // Where the main typeDef doesn't provide enough information
   protected getRawTypeDef(type: keyof InterfaceTypes) {

+ 4 - 3
cli/src/base/ContentDirectoryCommandBase.ts

@@ -25,6 +25,7 @@ import { createType } from '@joystream/types'
 import chalk from 'chalk'
 import { flags } from '@oclif/command'
 import { DistinctQuestion } from 'inquirer'
+import { memberHandle } from '../helpers/display'
 
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 type Context = typeof CONTEXTS[number]
@@ -157,7 +158,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     const choices = curators
       .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
       .map((c) => ({
-        name: `${c.profile.handle_hash.toString()} (Worker ID: ${c.workerId})`,
+        name: `${memberHandle(c.profile)} (Worker ID: ${c.workerId})`,
         value: c.workerId.toNumber(),
       }))
 
@@ -382,8 +383,8 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
   async getActor(context: typeof CONTEXTS[number], pickedClass: Class) {
     let actor: Actor
     if (context === 'Member') {
-      const [memberId] = await this.getRequiredMemberContext()
-      actor = this.createType('Actor', { Member: memberId })
+      const { id } = await this.getRequiredMemberContext()
+      actor = this.createType('Actor', { Member: id })
     } else if (context === 'Curator') {
       actor = await this.getCuratorContext([pickedClass.name.toString()])
     } else {

+ 2 - 0
cli/src/base/StateAwareCommandBase.ts

@@ -11,6 +11,7 @@ import { WorkingGroups } from '../Types'
 // Type for the state object (which is preserved as json in the state file)
 type StateObject = {
   apiUri: string
+  queryNodeUri: string
   defaultWorkingGroup: WorkingGroups
   metadataCache: Record<string, any>
 }
@@ -18,6 +19,7 @@ type StateObject = {
 // State object default values
 const DEFAULT_STATE: StateObject = {
   apiUri: '',
+  queryNodeUri: '',
   defaultWorkingGroup: WorkingGroups.StorageProviders,
   metadataCache: {},
 }

+ 3 - 2
cli/src/base/WorkingGroupsCommandBase.ts

@@ -5,6 +5,7 @@ import { WorkingGroups, AvailableGroups, GroupMember, OpeningDetails, Applicatio
 import _ from 'lodash'
 import chalk from 'chalk'
 import { IConfig } from '@oclif/config'
+import { memberHandle } from '../helpers/display'
 
 /**
  * Abstract base class for commands that need to use gates based on user's roles
@@ -37,7 +38,7 @@ export abstract class RolesCommandBase extends AccountsCommandBase {
     const availableGroupMemberContexts = groupMembers.filter((m) =>
       expectedKeyType === 'Role'
         ? this.isKeyAvailable(m.roleAccount.toString())
-        : this.isKeyAvailable(m.profile.controller_account.toString())
+        : this.isKeyAvailable(m.profile.membership.controller_account.toString())
     )
 
     if (!availableGroupMemberContexts.length) {
@@ -92,7 +93,7 @@ export default abstract class WorkingGroupsCommandBase extends RolesCommandBase
       message: 'Select succesful applicants',
       type: 'checkbox',
       choices: opening.applications.map((a) => ({
-        name: ` ${a.applicationId}: ${a.member?.handle_hash.toString()}`,
+        name: ` ${a.applicationId}: ${memberHandle(a.member)}`,
         value: a.applicationId,
       })),
     })

+ 11 - 0
cli/src/commands/api/getQueryNodeEndpoint.ts

@@ -0,0 +1,11 @@
+import StateAwareCommandBase from '../../base/StateAwareCommandBase'
+import chalk from 'chalk'
+
+export default class ApiGetQueryNodeEndpoint extends StateAwareCommandBase {
+  static description = 'Get current query node endpoint'
+
+  async run() {
+    const currentEndpoint: string = this.getPreservedState().queryNodeUri
+    this.log(chalk.green(currentEndpoint))
+  }
+}

+ 37 - 0
cli/src/commands/api/setQueryNodeEndpoint.ts

@@ -0,0 +1,37 @@
+import chalk from 'chalk'
+import ApiCommandBase from '../../base/ApiCommandBase'
+import ExitCodes from '../../ExitCodes'
+
+type ApiSetQueryNodeEndpointArgs = { endpoint: string }
+
+export default class ApiSetQueryNodeEndpoint extends ApiCommandBase {
+  static description = 'Set query node endpoint'
+  static args = [
+    {
+      name: 'endpoint',
+      required: false,
+      description: 'Query node endpoint for the CLI to use',
+    },
+  ]
+
+  async init() {
+    await super.init()
+  }
+
+  async run() {
+    const { endpoint }: ApiSetQueryNodeEndpointArgs = this.parse(ApiSetQueryNodeEndpoint)
+      .args as ApiSetQueryNodeEndpointArgs
+    let newEndpoint = ''
+    if (endpoint) {
+      if (this.isQueryNodeUriValid(endpoint)) {
+        await this.setPreservedState({ queryNodeUri: endpoint })
+        newEndpoint = endpoint
+      } else {
+        this.error('Provided endpoint seems to be incorrect!', { exit: ExitCodes.InvalidInput })
+      }
+    } else {
+      newEndpoint = await this.promptForQueryNodeUri()
+    }
+    this.log(chalk.greenBright('Query node endpoint successfuly changed! New endpoint: ') + chalk.white(newEndpoint))
+  }
+}

+ 0 - 1
cli/src/commands/api/setUri.ts

@@ -15,7 +15,6 @@ export default class ApiSetUri extends ApiCommandBase {
   ]
 
   async init() {
-    this.forceSkipApiUriPrompt = true
     await super.init()
   }
 

+ 2 - 4
cli/src/commands/content-directory/curatorGroup.ts

@@ -1,7 +1,7 @@
 import { WorkingGroups } from '../../Types'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import chalk from 'chalk'
-import { displayCollapsedRow, displayHeader } from '../../helpers/display'
+import { displayCollapsedRow, displayHeader, memberHandle } from '../../helpers/display'
 
 export default class CuratorGroupCommand extends ContentDirectoryCommandBase {
   static description = 'Show Curator Group details by ID.'
@@ -32,9 +32,7 @@ export default class CuratorGroupCommand extends ContentDirectoryCommandBase {
     displayHeader(`Group Members (${members.length})`)
     this.log(
       members
-        .map((curator) =>
-          chalk.white(`${curator.profile.handle_hash.toString()} (WorkerID: ${curator.workerId.toString()})`)
-        )
+        .map((curator) => chalk.white(`${memberHandle(curator.profile)} (WorkerID: ${curator.workerId.toString()})`))
         .join(', ')
     )
   }

+ 2 - 3
cli/src/commands/working-groups/application.ts

@@ -1,6 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { displayCollapsedRow, displayHeader } from '../../helpers/display'
-import chalk from 'chalk'
+import { displayCollapsedRow, displayHeader, memberHandle } from '../../helpers/display'
 
 export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
   static description = 'Shows an overview of given application by Working Group Application ID'
@@ -24,7 +23,7 @@ export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
     displayHeader(`Details`)
     const applicationRow = {
       'Application ID': application.applicationId,
-      'Member handle': application.member?.handle_hash.toString() || chalk.red('NONE'),
+      'Member handle': memberHandle(application.member),
       'Role account': application.roleAccout.toString(),
       'Reward account': application.rewardAccount.toString(),
       'Staking account': application.stakingAccount.toString(),

+ 8 - 4
cli/src/commands/working-groups/apply.ts

@@ -16,7 +16,7 @@ export default class WorkingGroupsApply extends WorkingGroupsCommandBase {
 
   async run() {
     const { openingId } = this.parse(WorkingGroupsApply).args
-    const [memberId, member] = await this.getRequiredMemberContext()
+    const memberContext = await this.getRequiredMemberContext()
 
     const opening = await this.getApi().groupOpening(this.group, parseInt(openingId))
 
@@ -25,7 +25,11 @@ export default class WorkingGroupsApply extends WorkingGroupsCommandBase {
 
     let stakeParams: CreateInterface<Option<StakeParameters>> = null
     if (opening.stake) {
-      const stakingAccount = await this.promptForStakingAccount(opening.stake.value, memberId, member)
+      const stakingAccount = await this.promptForStakingAccount(
+        opening.stake.value,
+        memberContext.id,
+        memberContext.membership
+      )
 
       stakeParams = {
         stake: opening.stake.value,
@@ -39,12 +43,12 @@ export default class WorkingGroupsApply extends WorkingGroupsCommandBase {
     })
 
     await this.sendAndFollowNamedTx(
-      await this.getDecodedPair(member.controller_account.toString()),
+      await this.getDecodedPair(memberContext.membership.controller_account.toString()),
       apiModuleByGroup[this.group],
       'applyOnOpening',
       [
         this.createType('ApplyOnOpeningParameters', {
-          member_id: memberId,
+          member_id: memberContext.id,
           opening_id: openingId,
           role_account_id: roleAccount,
           reward_account_id: rewardAccount,

+ 2 - 3
cli/src/commands/working-groups/opening.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { displayTable, displayCollapsedRow, displayHeader, shortAddress } from '../../helpers/display'
+import { displayTable, displayCollapsedRow, displayHeader, shortAddress, memberHandle } from '../../helpers/display'
 import { formatBalance } from '@polkadot/util'
-import chalk from 'chalk'
 
 export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
   static description = 'Shows an overview of given working group opening by Working Group Opening ID'
@@ -47,7 +46,7 @@ export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
     displayHeader(`Applications (${opening.applications.length})`)
     const applicationsRows = opening.applications.map((a) => ({
       'ID': a.applicationId,
-      Member: a.member?.handle_hash.toString() || chalk.red('NONE'),
+      Member: memberHandle(a.member),
       'Role Acc': shortAddress(a.roleAccout),
       'Reward Acc': shortAddress(a.rewardAccount),
       'Staking Acc': a.stakingAccount ? shortAddress(a.stakingAccount) : 'NONE',

+ 3 - 3
cli/src/commands/working-groups/overview.ts

@@ -1,5 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { displayHeader, displayNameValueTable, displayTable, shortAddress } from '../../helpers/display'
+import { displayHeader, displayNameValueTable, displayTable, memberHandle, shortAddress } from '../../helpers/display'
 import { formatBalance } from '@polkadot/util'
 
 import chalk from 'chalk'
@@ -18,7 +18,7 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
     if (lead) {
       displayNameValueTable([
         { name: 'Member id:', value: lead.memberId.toString() },
-        { name: 'Member handle:', value: lead.profile.handle_hash.toString() },
+        { name: 'Member handle:', value: memberHandle(lead.profile) },
         { name: 'Role account:', value: lead.roleAccount.toString() },
       ])
     } else {
@@ -31,7 +31,7 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
     const membersRows = members.map((m) => ({
       'Worker id': m.workerId.toString(),
       'Member id': m.memberId.toString(),
-      'Member handle': m.profile.handle_hash.toString(),
+      'Member handle': memberHandle(m.profile),
       Stake: formatBalance(m.stake),
       'Reward': formatBalance(m.reward?.valuePerBlock),
       'Missed reward': formatBalance(m.reward?.totalMissed),

+ 1 - 1
cli/src/commands/working-groups/updateRoleAccount.ts

@@ -30,7 +30,7 @@ export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommand
     }
 
     await this.sendAndFollowNamedTx(
-      await this.getDecodedPair(worker.profile.controller_account.toString()),
+      await this.getDecodedPair(worker.profile.membership.controller_account.toString()),
       apiModuleByGroup[this.group],
       'updateRoleAccount',
       [worker.workerId, address]

+ 5 - 1
cli/src/helpers/display.ts

@@ -1,6 +1,6 @@
 import { cli, Table } from 'cli-ux'
 import chalk from 'chalk'
-import { NameValueObj } from '../Types'
+import { MemberDetails, NameValueObj } from '../Types'
 import { AccountId } from '@polkadot/types/interfaces'
 
 export function displayHeader(caption: string, placeholderSign = '_', size = 50) {
@@ -71,3 +71,7 @@ export function toFixedLength(text: string, length: number, spacesOnLeft = false
 export function shortAddress(address: AccountId | string): string {
   return address.toString().substr(0, 6) + '...' + address.toString().substr(-6)
 }
+
+export function memberHandle(member: MemberDetails): string {
+  return member.handle ? member.handle : member.membership.handle_hash.toHex().substr(0, 10) + '... (hash)'
+}

+ 63 - 1
yarn.lock

@@ -42,6 +42,25 @@
     tslib "^1.10.0"
     zen-observable "^0.8.14"
 
+"@apollo/client@^3.3.13":
+  version "3.3.13"
+  resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.3.13.tgz#0aadde13b80ce129d1261161413a9de2b0dec70e"
+  integrity sha512-usiVmXiOq0J/GpyIOIhlF8ItHpiPJObC7zoxLYPoOYj3G3OB0hCIcUKs3aTJ3ATW7u8QxvYgRaJg72NN7E1WOg==
+  dependencies:
+    "@graphql-typed-document-node/core" "^3.0.0"
+    "@types/zen-observable" "^0.8.0"
+    "@wry/context" "^0.6.0"
+    "@wry/equality" "^0.4.0"
+    fast-json-stable-stringify "^2.0.0"
+    graphql-tag "^2.12.0"
+    hoist-non-react-statics "^3.3.2"
+    optimism "^0.15.0"
+    prop-types "^15.7.2"
+    symbol-observable "^2.0.0"
+    ts-invariant "^0.7.0"
+    tslib "^1.10.0"
+    zen-observable "^0.8.14"
+
 "@apollo/protobufjs@^1.0.3":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.0.5.tgz#a78b726147efc0795e74c8cb8a11aafc6e02f773"
@@ -6361,6 +6380,13 @@
   dependencies:
     tslib "^1.14.1"
 
+"@wry/context@^0.6.0":
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.6.0.tgz#f903eceb89d238ef7e8168ed30f4511f92d83e06"
+  integrity sha512-sAgendOXR8dM7stJw3FusRxFHF/ZinU0lffsA2YTyyIOfic86JX02qlPqPVqJNZJPAxFt+2EE8bvq6ZlS0Kf+Q==
+  dependencies:
+    tslib "^2.1.0"
+
 "@wry/equality@^0.1.2":
   version "0.1.11"
   resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
@@ -6375,6 +6401,20 @@
   dependencies:
     tslib "^1.14.1"
 
+"@wry/equality@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.4.0.tgz#474491869a8d0590f4a33fd2a4850a77a0f63408"
+  integrity sha512-DxN/uawWfhRbgYE55zVCPOoe+jvsQ4m7PT1Wlxjyb/LCCLuU1UsucV2BbCxFAX8bjcSueFBbB5Qfj1Zfe8e7Fw==
+  dependencies:
+    tslib "^2.1.0"
+
+"@wry/trie@^0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.3.0.tgz#3245e74988c4e3033299e479a1bf004430752463"
+  integrity sha512-Yw1akIogPhAT6XPYsRHlZZIS0tIGmAl9EYXHi2scf7LPKKqdqmow/Hu4kEqP2cJR3EjaU/9L0ZlAjFf3hFxmug==
+  dependencies:
+    tslib "^2.1.0"
+
 "@xtuc/ieee754@^1.2.0":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -14508,6 +14548,13 @@ graphql-tag@^2.11.0, graphql-tag@^2.9.2:
   resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
   integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==
 
+graphql-tag@^2.12.0:
+  version "2.12.3"
+  resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.3.tgz#ac47bf9d51c67c68ada8a33fd527143ed15bb647"
+  integrity sha512-5wJMjSvj30yzdciEuk9dPuUBUR56AqDi3xncoYQl1i42pGdSqOJrJsdb/rz5BDoy+qoGvQwABcBeF0xXY3TrKw==
+  dependencies:
+    tslib "^2.1.0"
+
 graphql-tools@4.0.5:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.5.tgz#d2b41ee0a330bfef833e5cdae7e1f0b0d86b1754"
@@ -21303,6 +21350,14 @@ optimism@^0.13.1:
   dependencies:
     "@wry/context" "^0.5.2"
 
+optimism@^0.15.0:
+  version "0.15.0"
+  resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.15.0.tgz#c65e694bec7ce439f41e9cb8fc261a72d798125b"
+  integrity sha512-KLKl3Kb7hH++s9ewRcBhmfpXgXF0xQ+JZ3xQFuPjnoT6ib2TDmYyVkKENmGxivsN2G3VRxpXuauCkB4GYOhtPw==
+  dependencies:
+    "@wry/context" "^0.6.0"
+    "@wry/trie" "^0.3.0"
+
 optimist@~0.3.5:
   version "0.3.7"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9"
@@ -27272,6 +27327,13 @@ ts-invariant@^0.6.0:
     "@ungap/global-this" "^0.4.2"
     tslib "^1.9.3"
 
+ts-invariant@^0.7.0:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.7.3.tgz#13aae22a4a165393aaf5cecdee45ef4128d358b8"
+  integrity sha512-UWDDeovyUTIMWj+45g5nhnl+8oo+GhxL5leTaHn5c8FkQWfh8v66gccLd2/YzVmV5hoQUjCEjhrXnQqVDJdvKA==
+  dependencies:
+    tslib "^2.1.0"
+
 ts-jest@^24.1.0:
   version "24.3.0"
   resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869"
@@ -27415,7 +27477,7 @@ tslib@^1, tslib@^1.10.0, tslib@^1.11.1, tslib@^1.13.0, tslib@^1.14.1, tslib@^1.8
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
-tslib@^2, tslib@~2.1.0:
+tslib@^2, tslib@^2.1.0, tslib@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
   integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==