Browse Source

Membership mappings - invitations, staking accounts, membership module config

Leszek Wiesner 4 years ago
parent
commit
d5e03b9cf5

+ 2 - 2
query-node/codegen/package.json

@@ -5,7 +5,7 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "@dzlzv/hydra-cli": "2.0.1-beta.11",
-    "@dzlzv/hydra-typegen": "2.0.1-beta.11"
+    "@dzlzv/hydra-cli": "2.0.1-beta.15",
+    "@dzlzv/hydra-typegen": "2.0.1-beta.15"
   }
 }

+ 8 - 8
query-node/codegen/yarn.lock

@@ -76,10 +76,10 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@dzlzv/hydra-cli@2.0.1-beta.11":
-  version "2.0.1-beta.11"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-cli/-/hydra-cli-2.0.1-beta.11.tgz#be4c2d242eb1470a7c6c33baa6b5fee2dd99224a"
-  integrity sha512-TaddlTrNfATxcFetvMmxCMiFpRbOfw3BxzcmahAwYg8jCb18KbA55FHt8N1/7mB9r/6F9NYqYGmSmqwAQCoiUA==
+"@dzlzv/hydra-cli@2.0.1-beta.15":
+  version "2.0.1-beta.15"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-cli/-/hydra-cli-2.0.1-beta.15.tgz#8d4c21e95835e83e84acc76b917c8e069aec6c48"
+  integrity sha512-GE6D7YKMzQOyinaxaDDrASeraH4jmZrTIGCwlTtADdz/f5Qq50mbMECiJq4Fi0LJfh0RTuOUU/Zk9thCtaVNnA==
   dependencies:
     "@inquirer/input" "^0.0.13-alpha.0"
     "@inquirer/password" "^0.0.12-alpha.0"
@@ -109,10 +109,10 @@
     tslib "1.11.2"
     warthog "https://github.com/metmirr/warthog/releases/download/v2.23.0/warthog-v2.23.0.tgz"
 
-"@dzlzv/hydra-typegen@2.0.1-beta.11":
-  version "2.0.1-beta.11"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-typegen/-/hydra-typegen-2.0.1-beta.11.tgz#9e802b00b07df38d0088f02571a7aedd3dcf8858"
-  integrity sha512-U11pUsukihj5/UF4/bKZFkg+FNGWZm/sGsHIDjOGGrFgC5kpW0BBNpL2VYkqw+i0fc41ZVvCtIPxHO/07zvIDQ==
+"@dzlzv/hydra-typegen@2.0.1-beta.15":
+  version "2.0.1-beta.15"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-typegen/-/hydra-typegen-2.0.1-beta.15.tgz#daa98df326d6e4871e7b710350c03ae076ada768"
+  integrity sha512-/xrmve0+DCP7HDsN9r/z33WGjMqq6fZJwwdoKX3+0Zdj+MSaB0sQu66sLQbPHe74k9x+6oRft2/kLQgFy2IgrA==
   dependencies:
     "@oclif/command" "^1.8.0"
     "@oclif/config" "^1"

+ 24 - 1
query-node/manifest.yml

@@ -16,8 +16,15 @@ typegen:
     - members.MemberProfileUpdated
     - members.MemberAccountsUpdated
     - members.MemberVerificationStatusUpdated
+    - members.InvitesTransferred
+    - members.MemberInvited
+    - members.StakingAccountConfirmed
+    - members.StakingAccountRemoved
+    - members.InitialInvitationCountUpdated
+    - members.MembershipPriceUpdated
+    - members.ReferralCutUpdated
+    - members.InitialInvitationBalanceUpdated
   calls:
-    - members.buyMembership
     - members.updateProfile
     - members.updateAccounts
   outDir: ./mappings/generated/types
@@ -43,6 +50,22 @@ mappings:
       handler: members_MemberAccountsUpdated(DatabaseManager, SubstrateEvent)
     - event: members.MemberVerificationStatusUpdated
       handler: members_MemberVerificationStatusUpdated(DatabaseManager, SubstrateEvent)
+    - event: members.InvitesTransferred
+      handler: members_InvitesTransferred(DatabaseManager, SubstrateEvent)
+    - event: members.MemberInvited
+      handler: members_MemberInvited(DatabaseManager, SubstrateEvent)
+    - event: members.StakingAccountConfirmed
+      handler: members_StakingAccountConfirmed(DatabaseManager, SubstrateEvent)
+    - event: members.StakingAccountRemoved
+      handler: members_StakingAccountRemoved(DatabaseManager, SubstrateEvent)
+    - event: members.InitialInvitationCountUpdated
+      handler: members_InitialInvitationCountUpdated(DatabaseManager, SubstrateEvent)
+    - event: members.MembershipPriceUpdated
+      handler: members_MembershipPriceUpdated(DatabaseManager, SubstrateEvent)
+    - event: members.ReferralCutUpdated
+      handler: members_ReferralCutUpdated(DatabaseManager, SubstrateEvent)
+    - event: members.InitialInvitationBalanceUpdated
+      handler: members_InitialInvitationBalanceUpdated(DatabaseManager, SubstrateEvent)
   extrinsicHandlers:
     # infer defaults here
     #- extrinsic: Balances.Transfer

+ 36 - 0
query-node/mappings/init.ts

@@ -0,0 +1,36 @@
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { types } from '@joystream/types'
+import { makeDatabaseManager } from '@dzlzv/hydra-db-utils'
+import { createDBConnection } from '@dzlzv/hydra-processor'
+import { MembershipSystem } from 'query-node/dist/src/modules/membership-system/membership-system.model'
+import path from 'path'
+
+// Temporary script to initialize processor database with some confing values initially hardcoded in the runtime
+async function init() {
+  const provider = new WsProvider(process.env.WS_PROVIDER_ENDPOINT_URI)
+  const api = await ApiPromise.create({ provider, types })
+  const entitiesPath = path.resolve(__dirname, '../../generated/graphql-server/dist/src/modules/**/*.model.js')
+  const dbConnection = await createDBConnection([entitiesPath])
+  const initialInvitationCount = await api.query.members.initialInvitationCount()
+  const initialInvitationBalance = await api.query.members.initialInvitationBalance()
+  const referralCut = await api.query.members.referralCut()
+  const membershipPrice = await api.query.members.membershipPrice()
+  const db = makeDatabaseManager(dbConnection.createEntityManager())
+  const membershipSystem = new MembershipSystem({
+    defaultInviteCount: initialInvitationCount.toNumber(),
+    membershipPrice,
+    referralCut,
+    invitedInitialBalance: initialInvitationBalance,
+  })
+  await db.save<MembershipSystem>(membershipSystem)
+}
+
+init()
+  .then(() => {
+    console.log('Processor database initialized')
+    process.exit()
+  })
+  .catch((e) => {
+    console.error(e)
+    process.exit(-1)
+  })

+ 124 - 11
query-node/mappings/mappings.ts

@@ -3,36 +3,52 @@ eslint-disable @typescript-eslint/naming-convention
 */
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
-import { MemberId } from '@joystream/types/common'
 import { Membership, MembershipEntryMethod } from 'query-node/dist/src/modules/membership/membership.model'
 import { Members } from './generated/types'
 import { prepareBlock } from './common'
 import BN from 'bn.js'
 import { Block } from 'query-node/dist/src/modules/block/block.model'
 import { Bytes } from '@polkadot/types'
+import { MembershipSystem } from 'query-node/dist/src/modules/membership-system/membership-system.model'
+import { MemberId, BuyMembershipParameters, InviteMembershipParameters } from '@joystream/types/augment/all'
 
 async function getMemberById(db: DatabaseManager, id: MemberId): Promise<Membership> {
   const member = await db.get(Membership, { where: { id: id.toString() } })
-  if (!member) throw Error(`Member(${id}) not found`)
+  if (!member) {
+    throw new Error(`Member(${id}) not found`)
+  }
   return member
 }
 
+async function getMembershipSystem(db: DatabaseManager) {
+  const membershipSystem = await db.get(MembershipSystem, {})
+  if (!membershipSystem) {
+    throw new Error(`Membership system entity not found! Forgot to run "yarn workspace query-node-root db:init"?`)
+  }
+  return membershipSystem
+}
+
 function bytesToString(b: Bytes): string {
   return Buffer.from(b.toU8a(true)).toString()
 }
 
-export async function members_MembershipBought(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+async function newMembershipFromParams(
+  db: DatabaseManager,
+  event_: SubstrateEvent,
+  memberId: MemberId,
+  entryMethod: MembershipEntryMethod,
+  params: BuyMembershipParameters | InviteMembershipParameters
+): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
-  const { memberId } = new Members.MembershipBoughtEvent(event_).data
+  const membershipSystem = await getMembershipSystem(db)
   const {
     name,
-    handle,
-    avatar_uri: avatarUri,
-    about,
     root_account: rootAccount,
     controller_account: controllerAccount,
-    referrer_id: referrerId,
-  } = new Members.BuyMembershipCall(event_).args.params
+    handle,
+    about,
+    avatar_uri: avatarUri,
+  } = params
   const member = new Membership({
     id: memberId.toString(),
     name: name.unwrapOr(undefined)?.toString(),
@@ -43,15 +59,31 @@ export async function members_MembershipBought(db: DatabaseManager, event_: Subs
     avatarUri: avatarUri.unwrapOr(undefined)?.toString(),
     registeredAtBlock: await prepareBlock(db, event_),
     registeredAtTime: new Date(event_.blockTimestamp.toNumber()),
-    entry: MembershipEntryMethod.PAID,
-    referrerId: referrerId.unwrapOr(undefined)?.toString(),
+    entry: entryMethod,
+    referredBy:
+      entryMethod === MembershipEntryMethod.PAID && (params as BuyMembershipParameters).referrer_id.isSome
+        ? new Membership({ id: (params as BuyMembershipParameters).referrer_id.unwrap().toString() })
+        : undefined,
     isVerified: false,
+    inviteCount: membershipSystem.defaultInviteCount,
+    boundAccounts: [],
+    invitees: [],
+    referredMembers: [],
+    invitedBy:
+      entryMethod === MembershipEntryMethod.INVITED
+        ? new Membership({ id: (params as InviteMembershipParameters).inviting_member_id.toString() })
+        : undefined,
   })
 
   await db.save<Block>(member.registeredAtBlock)
   await db.save<Membership>(member)
 }
 
+export async function members_MembershipBought(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { memberId, buyMembershipParameters } = new Members.MembershipBoughtEvent(event_).data
+  await newMembershipFromParams(db, event_, memberId, MembershipEntryMethod.PAID, buyMembershipParameters)
+}
+
 export async function members_MemberProfileUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const { memberId } = new Members.MemberProfileUpdatedEvent(event_).data
   const { name, about, avatarUri, handle } = new Members.UpdateProfileCall(event_).args
@@ -96,3 +128,84 @@ export async function members_MemberVerificationStatusUpdated(
 
   await db.save<Membership>(member)
 }
+
+export async function members_InvitesTransferred(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const {
+    memberIds: { 0: sourceMemberId, 1: targetMemberId },
+    u32: numberOfInvites,
+  } = new Members.InvitesTransferredEvent(event_).data
+  const sourceMember = await getMemberById(db, sourceMemberId)
+  const targetMember = await getMemberById(db, targetMemberId)
+  sourceMember.inviteCount -= numberOfInvites.toNumber()
+  targetMember.inviteCount += numberOfInvites.toNumber()
+
+  await db.save<Membership>(sourceMember)
+  await db.save<Membership>(targetMember)
+}
+
+export async function members_MemberInvited(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { memberId, inviteMembershipParameters } = new Members.MemberInvitedEvent(event_).data
+  await newMembershipFromParams(db, event_, memberId, MembershipEntryMethod.INVITED, inviteMembershipParameters)
+
+  // Decrease invite count of inviting member
+  const invitingMember = await getMemberById(db, inviteMembershipParameters.inviting_member_id)
+  invitingMember.inviteCount -= 1
+  await db.save<Membership>(invitingMember)
+}
+
+export async function members_StakingAccountConfirmed(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { memberId, accountId } = new Members.StakingAccountConfirmedEvent(event_).data
+  const member = await getMemberById(db, memberId)
+  member.boundAccounts.push(accountId.toString())
+
+  await db.save<Membership>(member)
+}
+
+export async function members_StakingAccountRemoved(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { memberId, accountId } = new Members.StakingAccountRemovedEvent(event_).data
+  const member = await getMemberById(db, memberId)
+  member.boundAccounts.splice(
+    member.boundAccounts.findIndex((a) => a === accountId.toString()),
+    1
+  )
+
+  await db.save<Membership>(member)
+}
+
+export async function members_InitialInvitationCountUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  const { u32: newDefaultInviteCount } = new Members.InitialInvitationCountUpdatedEvent(event_).data
+  const membershipSystem = await getMembershipSystem(db)
+  membershipSystem.defaultInviteCount = newDefaultInviteCount.toNumber()
+
+  await db.save<MembershipSystem>(membershipSystem)
+}
+
+export async function members_MembershipPriceUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { balance: newMembershipPrice } = new Members.MembershipPriceUpdatedEvent(event_).data
+  const membershipSystem = await getMembershipSystem(db)
+  membershipSystem.membershipPrice = newMembershipPrice
+
+  await db.save<MembershipSystem>(membershipSystem)
+}
+
+export async function members_ReferralCutUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { balance: newReferralCut } = new Members.ReferralCutUpdatedEvent(event_).data
+  const membershipSystem = await getMembershipSystem(db)
+  membershipSystem.referralCut = newReferralCut
+
+  await db.save<MembershipSystem>(membershipSystem)
+}
+
+export async function members_InitialInvitationBalanceUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  const { balance: newInvitedInitialBalance } = new Members.InitialInvitationBalanceUpdatedEvent(event_).data
+  const membershipSystem = await getMembershipSystem(db)
+  membershipSystem.invitedInitialBalance = newInvitedInitialBalance
+
+  await db.save<MembershipSystem>(membershipSystem)
+}

+ 2 - 2
query-node/mappings/package.json

@@ -10,8 +10,8 @@
     "clean": "rm -rf lib"
   },
   "dependencies": {
-    "@dzlzv/hydra-common": "2.0.1-beta.11",
-    "@dzlzv/hydra-db-utils": "2.0.1-beta.11",
+    "@dzlzv/hydra-common": "2.0.1-beta.15",
+    "@dzlzv/hydra-db-utils": "2.0.1-beta.15",
     "@joystream/types": "^0.15.0",
     "warthog": "https://github.com/metmirr/warthog/releases/download/v2.23.0/warthog-v2.23.0.tgz"
   },

+ 7 - 5
query-node/mappings/tsconfig.json

@@ -12,11 +12,13 @@
     "esModuleInterop": true,
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
-    "skipLibCheck": true
-    // "paths": {
-    //   "query-node": [ "../generated/graphql-server/src" ],
-    //   "query-node/*": [ "../generated/graphql-server/src/*" ]
-    // }
+    "skipLibCheck": true,
+    "paths": {
+      "@polkadot/types/augment": ["../../types/augment/augment-types.ts"],
+      "@polkadot/api/augment": ["../../types/augment/augment-api.ts"]
+      // "query-node": [ "../generated/graphql-server/src" ],
+      // "query-node/*": [ "../generated/graphql-server/src/*" ]
+    }
   },
   "include": ["./**/*"]
 }

+ 3 - 2
query-node/package.json

@@ -32,7 +32,8 @@
     "docker:db:up": "(cd ../ && docker-compose up -d db)",
     "docker:db:migrate": "docker run --env-file .env --env DB_HOST=db --env TYPEORM_HOST=db --network container:${PWD##*/}_db_1 hydra-kit:latest yarn db:migrate",
     "docker:up": "docker-compose up -d",
-    "format": "prettier ./ --write"
+    "format": "prettier ./ --write",
+    "db:init": "node --unhandled-rejections=strict ./mappings/lib/init.js"
   },
   "author": "",
   "license": "ISC",
@@ -40,7 +41,7 @@
     "tslib": "^2.0.0",
     "@types/bn.js": "^4.11.6",
     "bn.js": "^5.1.2",
-    "@dzlzv/hydra-processor": "2.0.1-beta.11",
+    "@dzlzv/hydra-processor": "2.0.1-beta.15",
     "envsub": "4.0.7"
   },
   "volta": {

+ 3 - 0
query-node/run-tests.sh

@@ -40,6 +40,9 @@ docker-compose up -d db
 yarn workspace query-node-root db:prepare
 yarn workspace query-node-root db:migrate
 
+# Initialize databse (ie. membership module configuration)
+yarn workspace query-node-root db:init
+
 docker-compose up -d graphql-server
 
 # Starting up processor will bring up all services it depends on

+ 33 - 3
query-node/schema.graphql

@@ -15,6 +15,7 @@ type Block @entity {
 
 enum MembershipEntryMethod {
   PAID
+  INVITED
   GENESIS
 }
 
@@ -50,9 +51,38 @@ type Membership @entity {
   "How the member was registered"
   entry: MembershipEntryMethod!
 
-  "Member id of the referrer (if any)"
-  referrerId: String
-
   "Whether member has been verified by membership working group."
   isVerified: Boolean!
+
+  "Staking accounts bounded to membership."
+  boundAccounts: [String!]
+
+  "Current count of invites left to send."
+  inviteCount: Int!
+
+  "All members invited by this member."
+  invitees: [Membership!] @derivedFrom(field: "invitedBy")
+
+  "A member that invited this member (if any)"
+  invitedBy: Membership
+
+  "All members referred by this member"
+  referredMembers: [Membership!] @derivedFrom(field: "referredBy")
+
+  "A member that referred this member (if any)"
+  referredBy: Membership
+}
+
+type MembershipSystem @entity {
+  "Initial invitation count of a new member."
+  defaultInviteCount: Int!
+
+  "Current price to buy a membership."
+  membershipPrice: BigInt!
+
+  "Amount of tokens diverted to invitor."
+  referralCut: BigInt!
+
+  "The initial, locked, balance credited to controller account of invitee."
+  invitedInitialBalance: BigInt!
 }

+ 4 - 0
tests/integration-tests/.env

@@ -8,6 +8,10 @@ TREASURY_ACCOUNT_URI = //Alice
 SUDO_ACCOUNT_URI = //Alice
 # Amount of members able to buy membership in membership creation test.
 MEMBERSHIP_CREATION_N = 2
+# Amount of members able to invite in members invite test.
+MEMBERS_INVITE_N = 2
+# Amount of staking accounts to add during "add staking accounts" test
+STAKING_ACCOUNTS_ADD_N = 3
 # ID of the membership paid terms used in membership creation test.
 MEMBERSHIP_PAID_TERMS = 0
 # Council stake amount for first K accounts in council election test.

+ 7 - 0
tests/integration-tests/src/Api.ts

@@ -219,6 +219,13 @@ export class Api {
     }
   }
 
+  public findMemberInvitedEvent(events: EventRecord[]): MemberId | undefined {
+    const record = this.findEventRecord(events, 'members', 'MemberInvited')
+    if (record) {
+      return record.event.data[0] as MemberId
+    }
+  }
+
   public getErrorNameFromExtrinsicFailedRecord(result: ISubmittableResult): string | undefined {
     const failed = result.findRecord('system', 'ExtrinsicFailed')
     if (!failed) {

+ 9 - 1
tests/integration-tests/src/QueryNodeApi.ts

@@ -54,6 +54,7 @@ export class QueryNodeApi {
     const MEMBER_BY_ID_QUERY = gql`
       query($id: ID!) {
         membership(where: { id: $id }) {
+          id
           handle
           name
           avatarUri
@@ -67,8 +68,15 @@ export class QueryNodeApi {
           }
           registeredAtTime
           entry
-          referrerId
           isVerified
+          inviteCount
+          invitedBy {
+            id
+          }
+          invitees {
+            id
+          }
+          boundAccounts
         }
       }
     `

+ 179 - 11
tests/integration-tests/src/QueryNodeApiSchema.generated.ts

@@ -11,6 +11,8 @@ export type Scalars = {
   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 = {
@@ -215,10 +217,18 @@ export type Membership = BaseGraphQlObject & {
   registeredAtTime: Scalars['DateTime']
   /** How the member was registered */
   entry: MembershipEntryMethod
-  /** Member id of the referrer (if any) */
-  referrerId?: Maybe<Scalars['String']>
   /** 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 = {
@@ -238,8 +248,11 @@ export type MembershipCreateInput = {
   registeredAtBlockId: Scalars['ID']
   registeredAtTime: Scalars['DateTime']
   entry: MembershipEntryMethod
-  referrerId?: Maybe<Scalars['String']>
   isVerified: Scalars['Boolean']
+  boundAccounts: Array<Scalars['String']>
+  inviteCount: Scalars['Float']
+  invitedById?: Maybe<Scalars['ID']>
+  referredById?: Maybe<Scalars['ID']>
 }
 
 export type MembershipEdge = {
@@ -250,6 +263,7 @@ export type MembershipEdge = {
 
 export enum MembershipEntryMethod {
   Paid = 'PAID',
+  Invited = 'INVITED',
   Genesis = 'GENESIS',
 }
 
@@ -278,10 +292,133 @@ export enum MembershipOrderByInput {
   RegisteredAtTimeDesc = 'registeredAtTime_DESC',
   EntryAsc = 'entry_ASC',
   EntryDesc = 'entry_DESC',
-  ReferrerIdAsc = 'referrerId_ASC',
-  ReferrerIdDesc = 'referrerId_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 = {
@@ -294,8 +431,11 @@ export type MembershipUpdateInput = {
   registeredAtBlockId?: Maybe<Scalars['ID']>
   registeredAtTime?: Maybe<Scalars['DateTime']>
   entry?: Maybe<MembershipEntryMethod>
-  referrerId?: Maybe<Scalars['String']>
   isVerified?: Maybe<Scalars['Boolean']>
+  boundAccounts?: Maybe<Array<Scalars['String']>>
+  inviteCount?: Maybe<Scalars['Float']>
+  invitedById?: Maybe<Scalars['ID']>
+  referredById?: Maybe<Scalars['ID']>
 }
 
 export type MembershipWhereInput = {
@@ -362,13 +502,18 @@ export type MembershipWhereInput = {
   registeredAtTime_gte?: Maybe<Scalars['DateTime']>
   entry_eq?: Maybe<MembershipEntryMethod>
   entry_in?: Maybe<Array<MembershipEntryMethod>>
-  referrerId_eq?: Maybe<Scalars['String']>
-  referrerId_contains?: Maybe<Scalars['String']>
-  referrerId_startsWith?: Maybe<Scalars['String']>
-  referrerId_endsWith?: Maybe<Scalars['String']>
-  referrerId_in?: Maybe<Array<Scalars['String']>>
   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 = {
@@ -404,6 +549,9 @@ export type 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
@@ -430,6 +578,26 @@ export type QueryBlocksConnectionArgs = {
   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']>

+ 244 - 23
tests/integration-tests/src/fixtures/membershipModule.ts

@@ -12,8 +12,12 @@ import { blake2AsHex } from '@polkadot/util-crypto'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { CreateInterface } from '@joystream/types'
 
+type MemberContext = {
+  account: string
+  memberId: MemberId
+}
 // common code for fixtures
-abstract class MembershipBuyer extends BaseFixture {
+abstract class MembershipFixture extends BaseFixture {
   generateParamsFromAccountId(accountId: string): CreateInterface<BuyMembershipParameters> {
     return {
       root_account: accountId,
@@ -32,9 +36,20 @@ abstract class MembershipBuyer extends BaseFixture {
   sendBuyMembershipTx(accountId: string): Promise<ISubmittableResult> {
     return this.api.signAndSend(this.generateBuyMembershipTx(accountId), accountId)
   }
+
+  generateInviteMemberTx(memberId: MemberId, inviteeAccountId: string): SubmittableExtrinsic<'promise'> {
+    return this.api.tx.members.inviteMember({
+      ...this.generateParamsFromAccountId(inviteeAccountId),
+      inviting_member_id: memberId,
+    })
+  }
+
+  sendInviteMemberTx(memberId: MemberId, inviterAccount: string, inviteeAccount: string): Promise<ISubmittableResult> {
+    return this.api.signAndSend(this.generateInviteMemberTx(memberId, inviteeAccount), inviterAccount)
+  }
 }
 
-export class BuyMembershipHappyCaseFixture extends MembershipBuyer implements BaseFixture {
+export class BuyMembershipHappyCaseFixture extends MembershipFixture implements BaseFixture {
   private accounts: string[]
   private debug: Debugger.Debugger
   private memberIds: MemberId[] = []
@@ -116,7 +131,7 @@ export class BuyMembershipHappyCaseFixture extends MembershipBuyer implements Ba
   }
 }
 
-export class BuyMembershipWithInsufficienFundsFixture extends MembershipBuyer implements BaseFixture {
+export class BuyMembershipWithInsufficienFundsFixture extends MembershipFixture implements BaseFixture {
   private account: string
 
   public constructor(api: Api, account: string) {
@@ -158,19 +173,17 @@ export class BuyMembershipWithInsufficienFundsFixture extends MembershipBuyer im
 
 export class UpdateProfileHappyCaseFixture extends BaseFixture {
   private query: QueryNodeApi
-  private memberController: string
-  private memberId: MemberId
+  private memberContext: MemberContext
   // Update data
   private newName = 'New name'
   private newHandle = 'New handle'
   private newAvatarUri = 'New avatar uri'
   private newAbout = 'New about'
 
-  public constructor(api: Api, query: QueryNodeApi, memberController: string, memberId: MemberId) {
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext) {
     super(api)
     this.query = query
-    this.memberController = memberController
-    this.memberId = memberId
+    this.memberContext = memberContext
   }
 
   private assertProfileUpdateSuccesful(qMember?: QueryNodeMembership | null) {
@@ -184,17 +197,17 @@ export class UpdateProfileHappyCaseFixture extends BaseFixture {
 
   async execute(): Promise<void> {
     const tx = this.api.tx.members.updateProfile(
-      this.memberId,
+      this.memberContext.memberId,
       this.newName,
       this.newHandle,
       this.newAvatarUri,
       this.newAbout
     )
-    const txFee = await this.api.estimateTxFee(tx, this.memberController)
-    await this.api.treasuryTransferBalance(this.memberController, txFee)
-    await this.api.signAndSend(tx, this.memberController)
+    const txFee = await this.api.estimateTxFee(tx, this.memberContext.account)
+    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
+    await this.api.signAndSend(tx, this.memberContext.account)
     await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(this.memberId),
+      () => this.query.getMemberById(this.memberContext.memberId),
       (res) => this.assertProfileUpdateSuccesful(res.data.membership)
     )
   }
@@ -202,17 +215,15 @@ export class UpdateProfileHappyCaseFixture extends BaseFixture {
 
 export class UpdateAccountsHappyCaseFixture extends BaseFixture {
   private query: QueryNodeApi
-  private memberController: string
-  private memberId: MemberId
+  private memberContext: MemberContext
   // Update data
   private newRootAccount: string
   private newControllerAccount: string
 
-  public constructor(api: Api, query: QueryNodeApi, memberController: string, memberId: MemberId) {
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext) {
     super(api)
     this.query = query
-    this.memberController = memberController
-    this.memberId = memberId
+    this.memberContext = memberContext
     const [newRootAccount, newControllerAccount] = this.api.createKeyPairs(2)
     this.newRootAccount = newRootAccount.address
     this.newControllerAccount = newControllerAccount.address
@@ -226,13 +237,223 @@ export class UpdateAccountsHappyCaseFixture extends BaseFixture {
   }
 
   async execute(): Promise<void> {
-    const tx = this.api.tx.members.updateAccounts(this.memberId, this.newRootAccount, this.newControllerAccount)
-    const txFee = await this.api.estimateTxFee(tx, this.memberController)
-    await this.api.treasuryTransferBalance(this.memberController, txFee)
-    await this.api.signAndSend(tx, this.memberController)
+    const tx = this.api.tx.members.updateAccounts(
+      this.memberContext.memberId,
+      this.newRootAccount,
+      this.newControllerAccount
+    )
+    const txFee = await this.api.estimateTxFee(tx, this.memberContext.account)
+    await this.api.treasuryTransferBalance(this.memberContext.account, txFee)
+    await this.api.signAndSend(tx, this.memberContext.account)
     await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(this.memberId),
+      () => this.query.getMemberById(this.memberContext.memberId),
       (res) => this.assertAccountsUpdateSuccesful(res.data.membership)
     )
   }
 }
+
+export class InviteMembersHappyCaseFixture extends MembershipFixture {
+  private query: QueryNodeApi
+  private inviterContext: MemberContext
+  private accounts: string[]
+
+  public constructor(api: Api, query: QueryNodeApi, inviterContext: MemberContext, accounts: string[]) {
+    super(api)
+    this.query = query
+    this.inviterContext = inviterContext
+    this.accounts = accounts
+  }
+
+  private assertMemberCorrectlyInvited(account: string, qMember?: QueryNodeMembership | null) {
+    assert.isOk(qMember, 'Membership query result is empty')
+    const {
+      handle,
+      rootAccount,
+      controllerAccount,
+      name,
+      about,
+      avatarUri,
+      isVerified,
+      entry,
+      invitedBy,
+    } = qMember as QueryNodeMembership
+    const txParams = this.generateParamsFromAccountId(account)
+    assert.equal(handle, txParams.handle)
+    assert.equal(rootAccount, txParams.root_account)
+    assert.equal(controllerAccount, txParams.controller_account)
+    assert.equal(name, txParams.name)
+    assert.equal(about, txParams.about)
+    assert.equal(avatarUri, txParams.avatar_uri)
+    assert.equal(isVerified, false)
+    assert.equal(entry, MembershipEntryMethod.Invited)
+    assert.isOk(invitedBy)
+    assert.equal(invitedBy!.id, this.inviterContext.memberId.toString())
+  }
+
+  async execute(): Promise<void> {
+    const exampleTx = this.generateInviteMemberTx(this.inviterContext.memberId, this.accounts[0])
+    const feePerTx = await this.api.estimateTxFee(exampleTx, this.inviterContext.account)
+    await this.api.treasuryTransferBalance(this.inviterContext.account, feePerTx.muln(this.accounts.length))
+
+    const initialInvitationBalance = await this.api.query.members.initialInvitationBalance()
+    // Top up working group budget to allow funding invited members
+    await this.api.makeSudoCall(
+      this.api.tx.membershipWorkingGroup.setBudget(initialInvitationBalance.muln(this.accounts.length))
+    )
+
+    const { invites: initialInvitesCount } = await this.api.query.members.membershipById(this.inviterContext.memberId)
+
+    const invitedMembersIds = (
+      await Promise.all(
+        this.accounts.map((account) =>
+          this.sendInviteMemberTx(this.inviterContext.memberId, this.inviterContext.account, account)
+        )
+      )
+    )
+      .map(({ events }) => this.api.findMemberInvitedEvent(events))
+      .filter((id) => id !== undefined) as MemberId[]
+
+    await Promise.all(
+      this.accounts.map((account, i) => {
+        const memberId = invitedMembersIds[i]
+        return this.query.tryQueryWithTimeout(
+          () => this.query.getMemberById(memberId),
+          (res) => this.assertMemberCorrectlyInvited(account, res.data.membership)
+        )
+      })
+    )
+
+    const {
+      data: { membership: inviter },
+    } = await this.query.getMemberById(this.inviterContext.memberId)
+    assert.isOk(inviter)
+    const { inviteCount, invitees } = inviter as QueryNodeMembership
+    // Assert that inviteCount was correctly updated
+    assert.equal(inviteCount, initialInvitesCount.toNumber() - this.accounts.length)
+    // Assert that all invited members are part of "invetees" field
+    assert.isNotEmpty(invitees)
+    assert.includeMembers(
+      invitees.map(({ id }) => id),
+      invitedMembersIds.map((id) => id.toString())
+    )
+  }
+}
+
+export class TransferInvitesHappyCaseFixture extends MembershipFixture {
+  private query: QueryNodeApi
+  private fromContext: MemberContext
+  private toContext: MemberContext
+  private invitesToTransfer: number
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    fromContext: MemberContext,
+    toContext: MemberContext,
+    invitesToTransfer = 2
+  ) {
+    super(api)
+    this.query = query
+    this.fromContext = fromContext
+    this.toContext = toContext
+    this.invitesToTransfer = invitesToTransfer
+  }
+
+  async execute(): Promise<void> {
+    const { fromContext, toContext, invitesToTransfer } = this
+    const tx = this.api.tx.members.transferInvites(fromContext.memberId, toContext.memberId, invitesToTransfer)
+    const txFee = await this.api.estimateTxFee(tx, fromContext.account)
+    await this.api.treasuryTransferBalance(fromContext.account, txFee)
+
+    const [fromMember, toMember] = await this.api.query.members.membershipById.multi<Membership>([
+      fromContext.memberId,
+      toContext.memberId,
+    ])
+
+    // Send transfer invites extrinsic
+    await this.api.signAndSend(tx, fromContext.account)
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(fromContext.memberId),
+      ({ data: { membership: queriedFromMember } }) => {
+        assert.isOk(queriedFromMember)
+        assert.equal(queriedFromMember!.inviteCount, fromMember.invites.toNumber() - invitesToTransfer)
+      }
+    )
+    const {
+      data: { membership: queriedToMember },
+    } = await this.query.getMemberById(toContext.memberId)
+    assert.isOk(queriedToMember)
+    assert.equal(queriedToMember!.inviteCount, toMember.invites.toNumber() + invitesToTransfer)
+  }
+}
+
+export class AddStakingAccountsHappyCaseFixture extends MembershipFixture {
+  private query: QueryNodeApi
+  private memberContext: MemberContext
+  private accounts: string[]
+
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
+    super(api)
+    this.query = query
+    this.memberContext = memberContext
+    this.accounts = accounts
+  }
+
+  async execute(): Promise<void> {
+    const { memberContext, accounts } = this
+    const addStakingCandidateTx = this.api.tx.members.addStakingAccountCandidate(memberContext.memberId)
+    const confirmStakingAccountTxs = accounts.map((a) =>
+      this.api.tx.members.confirmStakingAccount(memberContext.memberId, a)
+    )
+    const addStakingCandidateFee = await this.api.estimateTxFee(addStakingCandidateTx, accounts[0])
+    const confirmStakingAccountFee = await this.api.estimateTxFee(confirmStakingAccountTxs[0], memberContext.account)
+
+    await this.api.treasuryTransferBalance(memberContext.account, confirmStakingAccountFee.muln(accounts.length))
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, addStakingCandidateFee)))
+    // Add staking account candidates
+    await Promise.all(accounts.map((a) => this.api.signAndSend(addStakingCandidateTx, a)))
+    // Confirm staking accounts
+    await Promise.all(confirmStakingAccountTxs.map((tx) => this.api.signAndSend(tx, memberContext.account)))
+
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(memberContext.memberId),
+      ({ data: { membership } }) => {
+        assert.isOk(membership)
+        assert.isNotEmpty(membership!.boundAccounts)
+        assert.includeMembers(membership!.boundAccounts, accounts)
+      }
+    )
+  }
+}
+
+export class RemoveStakingAccountsHappyCaseFixture extends MembershipFixture {
+  private query: QueryNodeApi
+  private memberContext: MemberContext
+  private accounts: string[]
+
+  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
+    super(api)
+    this.query = query
+    this.memberContext = memberContext
+    this.accounts = accounts
+  }
+
+  async execute(): Promise<void> {
+    const { memberContext, accounts } = this
+    const removeStakingAccountTx = this.api.tx.members.removeStakingAccount(memberContext.memberId)
+
+    const removeStakingAccountFee = await this.api.estimateTxFee(removeStakingAccountTx, accounts[0])
+
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, removeStakingAccountFee)))
+    // Remove staking accounts
+    await Promise.all(accounts.map((a) => this.api.signAndSend(removeStakingAccountTx, a)))
+
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getMemberById(memberContext.memberId),
+      ({ data: { membership } }) => {
+        assert.isOk(membership)
+        assert.notInclude(membership!.boundAccounts, accounts)
+      }
+    )
+  }
+}

+ 1 - 1
tests/integration-tests/src/flows/membership/creatingMemberships.ts

@@ -9,7 +9,7 @@ import { FixtureRunner } from '../../Fixture'
 import { assert } from 'chai'
 
 export default async function membershipCreation({ api, query, env }: FlowProps): Promise<void> {
-  const debug = Debugger('flow:memberships')
+  const debug = Debugger('flow:creating-members')
   debug('Started')
   api.enableDebugTxLogs()
 

+ 31 - 0
tests/integration-tests/src/flows/membership/invitingMembers.ts

@@ -0,0 +1,31 @@
+import { FlowProps } from '../../Flow'
+import { BuyMembershipHappyCaseFixture, InviteMembersHappyCaseFixture } from '../../fixtures/membershipModule'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { assert } from 'chai'
+
+export default async function membershipCreation({ api, query, env }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:inviting-members')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const N: number = +env.MEMBERS_INVITE_N!
+  assert(N > 0)
+
+  const [inviterAcc] = api.createKeyPairs(1).map((key) => key.address)
+  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [inviterAcc])
+  await new FixtureRunner(buyMembershipHappyCaseFixture).run()
+  const [inviterMemberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
+
+  const inviteesAccs = api.createKeyPairs(N).map((key) => key.address)
+  const inviteMembersHappyCaseFixture = new InviteMembersHappyCaseFixture(
+    api,
+    query,
+    { account: inviterAcc, memberId: inviterMemberId },
+    inviteesAccs
+  )
+  await new FixtureRunner(inviteMembersHappyCaseFixture).run()
+
+  debug('Done')
+}

+ 43 - 0
tests/integration-tests/src/flows/membership/managingStakingAccounts.ts

@@ -0,0 +1,43 @@
+import { FlowProps } from '../../Flow'
+import {
+  AddStakingAccountsHappyCaseFixture,
+  BuyMembershipHappyCaseFixture,
+  RemoveStakingAccountsHappyCaseFixture,
+} from '../../fixtures/membershipModule'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { assert } from 'chai'
+
+export default async function membershipCreation({ api, query, env }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:adding-staking-accounts')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const [account] = api.createKeyPairs(1).map((key) => key.address)
+  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
+  await new FixtureRunner(buyMembershipHappyCaseFixture).run()
+  const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
+
+  const N: number = +env.STAKING_ACCOUNTS_ADD_N!
+  assert(N > 0)
+
+  const stakingAccounts = api.createKeyPairs(N).map((k) => k.address)
+  const addStakingAccountsHappyCaseFixture = new AddStakingAccountsHappyCaseFixture(
+    api,
+    query,
+    { account, memberId },
+    stakingAccounts
+  )
+  await new FixtureRunner(addStakingAccountsHappyCaseFixture).run()
+
+  const removeStakingAccountsHappyCaseFixture = new RemoveStakingAccountsHappyCaseFixture(
+    api,
+    query,
+    { account, memberId },
+    stakingAccounts
+  )
+  await new FixtureRunner(removeStakingAccountsHappyCaseFixture).run()
+
+  debug('Done')
+}

+ 26 - 0
tests/integration-tests/src/flows/membership/transferringInvites.ts

@@ -0,0 +1,26 @@
+import { FlowProps } from '../../Flow'
+import { BuyMembershipHappyCaseFixture, TransferInvitesHappyCaseFixture } from '../../fixtures/membershipModule'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+
+export default async function membershipCreation({ api, query, env }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:transferring-invites')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const [fromAcc, toAcc] = api.createKeyPairs(2).map((key) => key.address)
+  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [fromAcc, toAcc])
+  await new FixtureRunner(buyMembershipHappyCaseFixture).run()
+  const [fromMemberId, toMemberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
+
+  const transferInvitesHappyCaseFixture = new TransferInvitesHappyCaseFixture(
+    api,
+    query,
+    { memberId: fromMemberId, account: fromAcc },
+    { memberId: toMemberId, account: toAcc }
+  )
+  await new FixtureRunner(transferInvitesHappyCaseFixture).run()
+
+  debug('Done')
+}

+ 6 - 3
tests/integration-tests/src/flows/membership/updatingAccounts.ts

@@ -9,11 +9,14 @@ export default async function profileUpdate({ api, query }: FlowProps): Promise<
   debug('Started')
   api.enableDebugTxLogs()
 
-  const [memberAcc] = api.createKeyPairs(1).map((key) => key.address)
-  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [memberAcc])
+  const [account] = api.createKeyPairs(1).map((key) => key.address)
+  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
-  const updateAccountsHappyCaseFixture = new UpdateAccountsHappyCaseFixture(api, query, memberAcc, memberId)
+  const updateAccountsHappyCaseFixture = new UpdateAccountsHappyCaseFixture(api, query, {
+    account,
+    memberId,
+  })
   await new FixtureRunner(updateAccountsHappyCaseFixture).run()
 
   debug('Done')

+ 6 - 3
tests/integration-tests/src/flows/membership/updatingProfile.ts

@@ -9,11 +9,14 @@ export default async function profileUpdate({ api, query }: FlowProps): Promise<
   debug('Started')
   api.enableDebugTxLogs()
 
-  const [memberAcc] = api.createKeyPairs(1).map((key) => key.address)
-  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [memberAcc])
+  const [account] = api.createKeyPairs(1).map((key) => key.address)
+  const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
-  const updateProfileHappyCaseFixture = new UpdateProfileHappyCaseFixture(api, query, memberAcc, memberId)
+  const updateProfileHappyCaseFixture = new UpdateProfileHappyCaseFixture(api, query, {
+    account,
+    memberId,
+  })
   await new FixtureRunner(updateProfileHappyCaseFixture).run()
 
   debug('Done')

+ 6 - 0
tests/integration-tests/src/scenarios/olympia.ts

@@ -1,10 +1,16 @@
 import creatingMemberships from '../flows/membership/creatingMemberships'
 import updatingMemberProfile from '../flows/membership/updatingProfile'
 import updatingMemberAccounts from '../flows/membership/updatingAccounts'
+import invitingMebers from '../flows/membership/invitingMembers'
+import transferringInvites from '../flows/membership/transferringInvites'
+import managingStakingAccounts from '../flows/membership/managingStakingAccounts'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job }) => {
   job('creating members', creatingMemberships)
   job('updating member profile', updatingMemberProfile)
   job('updating member accounts', updatingMemberAccounts)
+  job('inviting members', invitingMebers)
+  job('transferring invites', transferringInvites)
+  job('managing staking accounts', managingStakingAccounts)
 })

+ 15 - 15
yarn.lock

@@ -1564,19 +1564,19 @@
     ajv "^6.12.0"
     ajv-keywords "^3.4.1"
 
-"@dzlzv/hydra-common@2.0.1-beta.11", "@dzlzv/hydra-common@^2.0.1-beta.11":
-  version "2.0.1-beta.11"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-common/-/hydra-common-2.0.1-beta.11.tgz#e18194312a9072311b867d013c6a5cc498f39591"
-  integrity sha512-2tE6seo7GUmNv89iLbypWmDtTf7PYqa7/qaJc7Ke6XUpqi+UEM0SA4AJZZPSj8yRLrxf3EEqO0cyZ7+sgEq3hQ==
+"@dzlzv/hydra-common@2.0.1-beta.15", "@dzlzv/hydra-common@^2.0.1-beta.15":
+  version "2.0.1-beta.15"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-common/-/hydra-common-2.0.1-beta.15.tgz#c498ed013684f2e07a1da4e3ba39205aef779614"
+  integrity sha512-tXP+hWGY/8TJVDG/Vpj5HIP/7snd4TS6PqIVWqZgThVNzOFwZUSGA3kbWRkx/kni9xNOEHJv0fLHYrgVIRTg5A==
   dependencies:
     bn.js "^5.1.3"
 
-"@dzlzv/hydra-db-utils@2.0.1-beta.11", "@dzlzv/hydra-db-utils@^2.0.1-beta.11":
-  version "2.0.1-beta.11"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-db-utils/-/hydra-db-utils-2.0.1-beta.11.tgz#9f52637310d69156b21fb55ad5be4709e4e13518"
-  integrity sha512-+zNKsLPsexNDyaiarYAJYmDiVNpbkloPenCuLiZILDEABi8+Sffk5/14MPy9nArrzpl+7NWGoCN/mdWwkMYmaA==
+"@dzlzv/hydra-db-utils@2.0.1-beta.15", "@dzlzv/hydra-db-utils@^2.0.1-beta.15":
+  version "2.0.1-beta.15"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-db-utils/-/hydra-db-utils-2.0.1-beta.15.tgz#eaf01ded617502cd3ac1968825862ecf962f0b4d"
+  integrity sha512-vZvXZU2p0rz02r1bavO4a+H+CbTM+23ikX4QWvClU5/HPRbjlkRln+GrwYcRNk+Aj1pl7yacJaWzdyLF4j0B+w==
   dependencies:
-    "@dzlzv/hydra-common" "^2.0.1-beta.11"
+    "@dzlzv/hydra-common" "^2.0.1-beta.15"
     "@types/ioredis" "^4.17.4"
     bn.js "^5.1.3"
     ioredis "^4.17.3"
@@ -1584,13 +1584,13 @@
     shortid "^2.2.16"
     typeorm "^0.2.25"
 
-"@dzlzv/hydra-processor@2.0.1-beta.11":
-  version "2.0.1-beta.11"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-processor/-/hydra-processor-2.0.1-beta.11.tgz#7d247b29f373d5548ebf99379a2d63b7e82139b3"
-  integrity sha512-AfhDL1+7pdPZRq7uG4KD/Ok47NkBo7h91CazGy2zfLmO0FQTKrUieei3DUic54MdXXrEfHcq75+sqPcXMbQ/qg==
+"@dzlzv/hydra-processor@2.0.1-beta.15":
+  version "2.0.1-beta.15"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-processor/-/hydra-processor-2.0.1-beta.15.tgz#e1a712eed3d310e2d5975a602b3603ab60d4724d"
+  integrity sha512-k3vo2fZKkdCUfTD2+5l+XgNSBcnXVfIbxgHg6D5VjeoR737AJMehRAVVkuzJXn0cmpl+qPXDwDP7V3dj7YjP1A==
   dependencies:
-    "@dzlzv/hydra-common" "^2.0.1-beta.11"
-    "@dzlzv/hydra-db-utils" "^2.0.1-beta.11"
+    "@dzlzv/hydra-common" "^2.0.1-beta.15"
+    "@dzlzv/hydra-db-utils" "^2.0.1-beta.15"
     "@oclif/command" "^1.8.0"
     "@oclif/config" "^1"
     "@oclif/errors" "^1.3.3"