Browse Source

MembershipSystemSnapshot + test coverage + a few small fixes

Leszek Wiesner 4 years ago
parent
commit
37d323ffa4

+ 5 - 7
query-node/mappings/common.ts

@@ -1,15 +1,13 @@
-/*
-eslint-disable @typescript-eslint/naming-convention
-*/
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { EventType } from 'query-node/dist/src/modules/enums/enums'
 import { Event } from 'query-node/dist/src/modules/event/event.model'
 
-export function createEvent(event_: SubstrateEvent, type: EventType): Event {
+export function createEvent({ blockNumber, extrinsic, index }: SubstrateEvent, type: EventType): Event {
   return new Event({
-    inBlock: event_.blockNumber,
-    inExtrinsic: event_.extrinsic?.hash,
-    indexInBlock: event_.index,
+    id: `${blockNumber}-${index}`,
+    inBlock: blockNumber,
+    inExtrinsic: extrinsic?.hash,
+    indexInBlock: index,
     type,
   })
 }

+ 9 - 7
query-node/mappings/init.ts

@@ -2,7 +2,7 @@ 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 { MembershipSystemSnapshot } from 'query-node/dist/src/modules/membership-system-snapshot/membership-system-snapshot.model'
 import path from 'path'
 
 // Temporary script to initialize processor database with some confing values initially hardcoded in the runtime
@@ -11,18 +11,20 @@ async function init() {
   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 initialInvitationCount = await api.query.members.initialInvitationCount.at(api.genesisHash)
+  const initialInvitationBalance = await api.query.members.initialInvitationBalance.at(api.genesisHash)
+  const referralCut = await api.query.members.referralCut.at(api.genesisHash)
+  const membershipPrice = await api.query.members.membershipPrice.at(api.genesisHash)
   const db = makeDatabaseManager(dbConnection.createEntityManager())
-  const membershipSystem = new MembershipSystem({
+  const membershipSystem = new MembershipSystemSnapshot({
+    snapshotBlock: 0,
+    snapshotTime: new Date(0),
     defaultInviteCount: initialInvitationCount.toNumber(),
     membershipPrice,
     referralCut: referralCut.toNumber(),
     invitedInitialBalance: initialInvitationBalance,
   })
-  await db.save<MembershipSystem>(membershipSystem)
+  await db.save<MembershipSystemSnapshot>(membershipSystem)
 }
 
 init()

+ 30 - 20
query-node/mappings/mappings.ts

@@ -8,7 +8,7 @@ import { Members } from './generated/types'
 import BN from 'bn.js'
 import { Bytes } from '@polkadot/types'
 import { EventType, MembershipEntryMethod } from 'query-node/dist/src/modules/enums/enums'
-import { MembershipSystem } from 'query-node/dist/src/modules/membership-system/membership-system.model'
+import { MembershipSystemSnapshot } from 'query-node/dist/src/modules/membership-system-snapshot/membership-system-snapshot.model'
 import { MemberMetadata } from 'query-node/dist/src/modules/member-metadata/member-metadata.model'
 import { MembershipBoughtEvent } from 'query-node/dist/src/modules/membership-bought-event/membership-bought-event.model'
 import { MemberProfileUpdatedEvent } from 'query-node/dist/src/modules/member-profile-updated-event/member-profile-updated-event.model'
@@ -37,14 +37,26 @@ async function getMemberById(db: DatabaseManager, id: MemberId): Promise<Members
   return member
 }
 
-async function getMembershipSystem(db: DatabaseManager) {
-  const membershipSystem = await db.get(MembershipSystem, {})
+async function getLatestMembershipSystemSnapshot(db: DatabaseManager): Promise<MembershipSystemSnapshot> {
+  const membershipSystem = await db.get(MembershipSystemSnapshot, { order: { snapshotBlock: 'DESC' } })
   if (!membershipSystem) {
-    throw new Error(`Membership system entity not found! Forgot to run "yarn workspace query-node-root db:init"?`)
+    throw new Error(`Membership system snapshot not found! Forgot to run "yarn workspace query-node-root db:init"?`)
   }
   return membershipSystem
 }
 
+async function getOrCreateMembershipSnapshot(db: DatabaseManager, event_: SubstrateEvent) {
+  const latestSnapshot = await getLatestMembershipSystemSnapshot(db)
+  return latestSnapshot.snapshotBlock === event_.blockNumber
+    ? latestSnapshot
+    : new MembershipSystemSnapshot({
+        ...latestSnapshot,
+        id: undefined,
+        snapshotBlock: event_.blockNumber,
+        snapshotTime: new Date(new BN(event_.blockTimestamp).toNumber()),
+      })
+}
+
 function bytesToString(b: Bytes): string {
   return Buffer.from(b.toU8a(true)).toString()
 }
@@ -66,7 +78,7 @@ async function newMembershipFromParams(
   params: BuyMembershipParameters | InviteMembershipParameters
 ): Promise<Membership> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
-  const membershipSystem = await getMembershipSystem(db)
+  const { defaultInviteCount } = await getLatestMembershipSystemSnapshot(db)
   const { root_account: rootAccount, controller_account: controllerAccount, handle, metadata: metatadaBytes } = params
   const metadata = deserializeMemberMeta(metatadaBytes)
 
@@ -90,7 +102,7 @@ async function newMembershipFromParams(
         ? new Membership({ id: (params as BuyMembershipParameters).referrer_id.unwrap().toString() })
         : undefined,
     isVerified: false,
-    inviteCount: membershipSystem.defaultInviteCount,
+    inviteCount: defaultInviteCount,
     boundAccounts: [],
     invitees: [],
     referredMembers: [],
@@ -325,10 +337,10 @@ export async function members_InitialInvitationCountUpdated(
   event_: SubstrateEvent
 ): Promise<void> {
   const { u32: newDefaultInviteCount } = new Members.InitialInvitationCountUpdatedEvent(event_).data
-  const membershipSystem = await getMembershipSystem(db)
-  membershipSystem.defaultInviteCount = newDefaultInviteCount.toNumber()
+  const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(db, event_)
+  membershipSystemSnapshot.defaultInviteCount = newDefaultInviteCount.toNumber()
 
-  await db.save<MembershipSystem>(membershipSystem)
+  await db.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
 
   const initialInvitationCountUpdatedEvent = new InitialInvitationCountUpdatedEvent({
     event: createEvent(event_, EventType.InitialInvitationCountUpdated),
@@ -341,10 +353,10 @@ export async function members_InitialInvitationCountUpdated(
 
 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
+  const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(db, event_)
+  membershipSystemSnapshot.membershipPrice = newMembershipPrice
 
-  await db.save<MembershipSystem>(membershipSystem)
+  await db.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
 
   const membershipPriceUpdatedEvent = new MembershipPriceUpdatedEvent({
     event: createEvent(event_, EventType.MembershipPriceUpdated),
@@ -357,10 +369,10 @@ export async function members_MembershipPriceUpdated(db: DatabaseManager, event_
 
 export async function members_ReferralCutUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const { u8: newReferralCut } = new Members.ReferralCutUpdatedEvent(event_).data
-  const membershipSystem = await getMembershipSystem(db)
-  membershipSystem.referralCut = newReferralCut.toNumber()
+  const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(db, event_)
+  membershipSystemSnapshot.referralCut = newReferralCut.toNumber()
 
-  await db.save<MembershipSystem>(membershipSystem)
+  await db.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
 
   const referralCutUpdatedEvent = new ReferralCutUpdatedEvent({
     event: createEvent(event_, EventType.ReferralCutUpdated),
@@ -376,10 +388,10 @@ export async function members_InitialInvitationBalanceUpdated(
   event_: SubstrateEvent
 ): Promise<void> {
   const { balance: newInvitedInitialBalance } = new Members.InitialInvitationBalanceUpdatedEvent(event_).data
-  const membershipSystem = await getMembershipSystem(db)
-  membershipSystem.invitedInitialBalance = newInvitedInitialBalance
+  const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(db, event_)
+  membershipSystemSnapshot.invitedInitialBalance = newInvitedInitialBalance
 
-  await db.save<MembershipSystem>(membershipSystem)
+  await db.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
 
   const initialInvitationBalanceUpdatedEvent = new InitialInvitationBalanceUpdatedEvent({
     event: createEvent(event_, EventType.InitialInvitationBalanceUpdated),
@@ -398,8 +410,6 @@ export async function members_LeaderInvitationQuotaUpdated(db: DatabaseManager,
     newInvitationQuota: newQuota.toNumber(),
   })
 
-  // TODO: Update MembershipSystem?
-
   await db.save<Event>(leaderInvitationQuotaUpdatedEvent.event)
   await db.save<LeaderInvitationQuotaUpdatedEvent>(leaderInvitationQuotaUpdatedEvent)
 }

+ 10 - 4
query-node/schema.graphql

@@ -78,7 +78,13 @@ type Membership @entity {
   referredBy: Membership
 }
 
-type MembershipSystem @entity {
+type MembershipSystemSnapshot @entity {
+  "Block number of the snapshot block"
+  snapshotBlock: Int!
+
+  "Time of the snapshot (based on block timestamp)"
+  snapshotTime: DateTime!
+
   "Initial invitation count of a new member."
   defaultInviteCount: Int!
 
@@ -112,6 +118,9 @@ enum EventType {
 }
 
 type Event @entity {
+  "{blockNumber}-{indexInBlock}"
+  id: ID!
+
   "Hash of the extrinsic which caused the event to be emitted"
   inExtrinsic: String
 
@@ -218,9 +227,6 @@ type ReferralCutUpdatedEvent @entity {
   "Generic event data"
   event: Event!
 
-  "Membership in question."
-  member: Membership!
-
   "New cut value."
   newValue: Int!
 }

+ 5 - 1
tests/integration-tests/src/Api.ts

@@ -4,7 +4,7 @@ import { ISubmittableResult } from '@polkadot/types/types'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { AccountId, MemberId } from '@joystream/types/common'
 
-import { AccountInfo, Balance, EventRecord } from '@polkadot/types/interfaces'
+import { AccountInfo, Balance, EventRecord, BlockNumber, BlockHash } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
 import { QueryableConsts, QueryableStorage, SubmittableExtrinsic, SubmittableExtrinsics } from '@polkadot/api/types'
 import { Sender, LogLevel } from './sender'
@@ -173,6 +173,10 @@ export class Api {
     return this.api.derive.chain.bestNumber()
   }
 
+  public async getBlockHash(blockNumber: number | BlockNumber): Promise<BlockHash> {
+    return this.api.rpc.chain.getBlockHash(blockNumber)
+  }
+
   public async getControllerAccountOfMember(id: MemberId): Promise<string> {
     return (await this.api.query.members.membershipById(id)).controller_account.toString()
   }

+ 161 - 6
tests/integration-tests/src/QueryNodeApi.ts

@@ -1,31 +1,43 @@
 import { gql, ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'
 import { MemberId } from '@joystream/types/common'
-import { Query } from './QueryNodeApiSchema.generated'
+import {
+  InitialInvitationBalanceUpdatedEvent,
+  InitialInvitationCountUpdatedEvent,
+  MembershipPriceUpdatedEvent,
+  MembershipSystemSnapshot,
+  Query,
+  ReferralCutUpdatedEvent,
+} from './QueryNodeApiSchema.generated'
 import Debugger from 'debug'
 
 export class QueryNodeApi {
   private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
+  private readonly debug: Debugger.Debugger
   private readonly queryDebug: Debugger.Debugger
+  private readonly tryDebug: Debugger.Debugger
 
   constructor(queryNodeProvider: ApolloClient<NormalizedCacheObject>) {
     this.queryNodeProvider = queryNodeProvider
-    this.queryDebug = Debugger('query-node-api:query')
+    this.debug = Debugger('query-node-api')
+    this.queryDebug = this.debug.extend('query')
+    this.tryDebug = this.debug.extend('try')
   }
 
-  public tryQueryWithTimeout<QueryResultT extends ApolloQueryResult<unknown>>(
+  public tryQueryWithTimeout<QueryResultT>(
     query: () => Promise<QueryResultT>,
     assertResultIsValid: (res: QueryResultT) => void,
     timeoutMs = 210000,
     retryTimeMs = 30000
   ): Promise<QueryResultT> {
-    const retryDebug = Debugger('query-node-api:retry')
+    const label = query.toString().replace(/^.*\./g, '')
+    const retryDebug = this.tryDebug.extend(label).extend('retry')
+    const failDebug = this.tryDebug.extend(label).extend('failed')
     return new Promise((resolve, reject) => {
       let lastError: any
       const timeout = setTimeout(() => {
-        console.error(`Query node query is still failing after timeout was reached (${timeoutMs}ms)!`)
+        failDebug(`Query node query is still failing after timeout was reached (${timeoutMs}ms)!`)
         reject(lastError)
       }, timeoutMs)
-
       const tryQuery = () => {
         query()
           .then((result) => {
@@ -340,4 +352,147 @@ export class QueryNodeApi {
       variables: { memberId: memberId.toNumber() },
     })
   }
+
+  public async getMembershipSystemSnapshot(
+    blockNumber: number,
+    matchType: 'eq' | 'lt' | 'lte' | 'gt' | 'gte' = 'eq'
+  ): Promise<MembershipSystemSnapshot | undefined> {
+    const MEMBERSHIP_SYSTEM_SNAPSHOT_QUERY = gql`
+      query($blockNumber: Int!) {
+        membershipSystemSnapshots(where: { snapshotBlock_${matchType}: $blockNumber }, orderBy: snapshotBlock_DESC, limit: 1) {
+          snapshotBlock,
+          snapshotTime,
+          referralCut,
+          invitedInitialBalance,
+          defaultInviteCount,
+          membershipPrice
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getMembershipSystemSnapshot(${matchType} ${blockNumber})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'membershipSystemSnapshots'>>({
+        query: MEMBERSHIP_SYSTEM_SNAPSHOT_QUERY,
+        variables: { blockNumber },
+      })
+    ).data.membershipSystemSnapshots[0]
+  }
+
+  public async getReferralCutUpdatedEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<ReferralCutUpdatedEvent | undefined> {
+    const REFERRAL_CUT_UPDATED_BY_ID = gql`
+      query($eventId: ID!) {
+        referralCutUpdatedEvents(where: { eventId_eq: $eventId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          newValue
+        }
+      }
+    `
+
+    const eventId = `${blockNumber}-${indexInBlock}`
+    this.queryDebug(`Executing getReferralCutUpdatedEvent(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'referralCutUpdatedEvents'>>({
+        query: REFERRAL_CUT_UPDATED_BY_ID,
+        variables: { eventId },
+      })
+    ).data.referralCutUpdatedEvents[0]
+  }
+
+  public async getMembershipPriceUpdatedEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<MembershipPriceUpdatedEvent | undefined> {
+    const MEMBERSHIP_PRICE_UPDATED_BY_ID = gql`
+      query($eventId: ID!) {
+        membershipPriceUpdatedEvents(where: { eventId_eq: $eventId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          newPrice
+        }
+      }
+    `
+
+    const eventId = `${blockNumber}-${indexInBlock}`
+    this.queryDebug(`Executing getMembershipPriceUpdatedEvent(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'membershipPriceUpdatedEvents'>>({
+        query: MEMBERSHIP_PRICE_UPDATED_BY_ID,
+        variables: { eventId },
+      })
+    ).data.membershipPriceUpdatedEvents[0]
+  }
+
+  public async getInitialInvitationBalanceUpdatedEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<InitialInvitationBalanceUpdatedEvent | undefined> {
+    const INITIAL_INVITATION_BALANCE_UPDATED_BY_ID = gql`
+      query($eventId: ID!) {
+        initialInvitationBalanceUpdatedEvents(where: { eventId_eq: $eventId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          newInitialBalance
+        }
+      }
+    `
+
+    const eventId = `${blockNumber}-${indexInBlock}`
+    this.queryDebug(`Executing getInitialInvitationBalanceUpdatedEvent(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'initialInvitationBalanceUpdatedEvents'>>({
+        query: INITIAL_INVITATION_BALANCE_UPDATED_BY_ID,
+        variables: { eventId },
+      })
+    ).data.initialInvitationBalanceUpdatedEvents[0]
+  }
+
+  public async getInitialInvitationCountUpdatedEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<InitialInvitationCountUpdatedEvent | undefined> {
+    const INITIAL_INVITATION_COUNT_UPDATED_BY_ID = gql`
+      query($eventId: ID!) {
+        initialInvitationCountUpdatedEvents(where: { eventId_eq: $eventId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          newInitialInvitationCount
+        }
+      }
+    `
+
+    const eventId = `${blockNumber}-${indexInBlock}`
+    this.queryDebug(`Executing getInitialInvitationCountUpdatedEvent(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'initialInvitationCountUpdatedEvents'>>({
+        query: INITIAL_INVITATION_COUNT_UPDATED_BY_ID,
+        variables: { eventId },
+      })
+    ).data.initialInvitationCountUpdatedEvents[0]
+  }
 }

+ 66 - 52
tests/integration-tests/src/QueryNodeApiSchema.generated.ts

@@ -957,8 +957,6 @@ export type MemberMetadata = BaseGraphQlObject & {
   deletedAt?: Maybe<Scalars['DateTime']>
   deletedById?: Maybe<Scalars['String']>
   version: Scalars['Int']
-  member: Membership
-  memberId: Scalars['String']
   /** Member's name */
   name?: Maybe<Scalars['String']>
   /** A Url to member's Avatar image TODO: Storage asset */
@@ -979,7 +977,6 @@ export type MemberMetadataConnection = {
 }
 
 export type MemberMetadataCreateInput = {
-  memberId: Scalars['ID']
   name?: Maybe<Scalars['String']>
   avatarUri?: Maybe<Scalars['String']>
   about?: Maybe<Scalars['String']>
@@ -998,8 +995,6 @@ export enum MemberMetadataOrderByInput {
   UpdatedAtDesc = 'updatedAt_DESC',
   DeletedAtAsc = 'deletedAt_ASC',
   DeletedAtDesc = 'deletedAt_DESC',
-  MemberIdAsc = 'memberId_ASC',
-  MemberIdDesc = 'memberId_DESC',
   NameAsc = 'name_ASC',
   NameDesc = 'name_DESC',
   AvatarUriAsc = 'avatarUri_ASC',
@@ -1009,7 +1004,6 @@ export enum MemberMetadataOrderByInput {
 }
 
 export type MemberMetadataUpdateInput = {
-  memberId?: Maybe<Scalars['ID']>
   name?: Maybe<Scalars['String']>
   avatarUri?: Maybe<Scalars['String']>
   about?: Maybe<Scalars['String']>
@@ -1040,8 +1034,6 @@ export type MemberMetadataWhereInput = {
   deletedAt_gte?: Maybe<Scalars['DateTime']>
   deletedById_eq?: Maybe<Scalars['ID']>
   deletedById_in?: Maybe<Array<Scalars['ID']>>
-  memberId_eq?: Maybe<Scalars['ID']>
-  memberId_in?: Maybe<Array<Scalars['ID']>>
   name_eq?: Maybe<Scalars['String']>
   name_contains?: Maybe<Scalars['String']>
   name_startsWith?: Maybe<Scalars['String']>
@@ -1221,7 +1213,6 @@ export type Membership = BaseGraphQlObject & {
   memberaccountsupdatedeventmember?: Maybe<Array<MemberAccountsUpdatedEvent>>
   memberinvitedeventinvitingMember?: Maybe<Array<MemberInvitedEvent>>
   memberinvitedeventnewMember?: Maybe<Array<MemberInvitedEvent>>
-  membermetadatamember?: Maybe<Array<MemberMetadata>>
   memberprofileupdatedeventmember?: Maybe<Array<MemberProfileUpdatedEvent>>
   memberverificationstatusupdatedeventmember?: Maybe<Array<MemberVerificationStatusUpdatedEvent>>
   membershipboughteventnewMember?: Maybe<Array<MembershipBoughtEvent>>
@@ -1524,8 +1515,8 @@ export type MembershipPriceUpdatedEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
-export type MembershipSystem = BaseGraphQlObject & {
-  __typename?: 'MembershipSystem'
+export type MembershipSystemSnapshot = BaseGraphQlObject & {
+  __typename?: 'MembershipSystemSnapshot'
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
   createdById: Scalars['String']
@@ -1534,43 +1525,53 @@ export type MembershipSystem = BaseGraphQlObject & {
   deletedAt?: Maybe<Scalars['DateTime']>
   deletedById?: Maybe<Scalars['String']>
   version: Scalars['Int']
+  /** Block number of the snapshot block */
+  snapshotBlock: Scalars['Int']
+  /** Time of the snapshot (based on block timestamp) */
+  snapshotTime: Scalars['DateTime']
   /** 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']
+  /** Percentage of tokens diverted to invitor. */
+  referralCut: Scalars['Int']
   /** The initial, locked, balance credited to controller account of invitee. */
   invitedInitialBalance: Scalars['BigInt']
 }
 
-export type MembershipSystemConnection = {
-  __typename?: 'MembershipSystemConnection'
+export type MembershipSystemSnapshotConnection = {
+  __typename?: 'MembershipSystemSnapshotConnection'
   totalCount: Scalars['Int']
-  edges: Array<MembershipSystemEdge>
+  edges: Array<MembershipSystemSnapshotEdge>
   pageInfo: PageInfo
 }
 
-export type MembershipSystemCreateInput = {
+export type MembershipSystemSnapshotCreateInput = {
+  snapshotBlock: Scalars['Float']
+  snapshotTime: Scalars['DateTime']
   defaultInviteCount: Scalars['Float']
   membershipPrice: Scalars['BigInt']
-  referralCut: Scalars['BigInt']
+  referralCut: Scalars['Float']
   invitedInitialBalance: Scalars['BigInt']
 }
 
-export type MembershipSystemEdge = {
-  __typename?: 'MembershipSystemEdge'
-  node: MembershipSystem
+export type MembershipSystemSnapshotEdge = {
+  __typename?: 'MembershipSystemSnapshotEdge'
+  node: MembershipSystemSnapshot
   cursor: Scalars['String']
 }
 
-export enum MembershipSystemOrderByInput {
+export enum MembershipSystemSnapshotOrderByInput {
   CreatedAtAsc = 'createdAt_ASC',
   CreatedAtDesc = 'createdAt_DESC',
   UpdatedAtAsc = 'updatedAt_ASC',
   UpdatedAtDesc = 'updatedAt_DESC',
   DeletedAtAsc = 'deletedAt_ASC',
   DeletedAtDesc = 'deletedAt_DESC',
+  SnapshotBlockAsc = 'snapshotBlock_ASC',
+  SnapshotBlockDesc = 'snapshotBlock_DESC',
+  SnapshotTimeAsc = 'snapshotTime_ASC',
+  SnapshotTimeDesc = 'snapshotTime_DESC',
   DefaultInviteCountAsc = 'defaultInviteCount_ASC',
   DefaultInviteCountDesc = 'defaultInviteCount_DESC',
   MembershipPriceAsc = 'membershipPrice_ASC',
@@ -1581,14 +1582,16 @@ export enum MembershipSystemOrderByInput {
   InvitedInitialBalanceDesc = 'invitedInitialBalance_DESC',
 }
 
-export type MembershipSystemUpdateInput = {
+export type MembershipSystemSnapshotUpdateInput = {
+  snapshotBlock?: Maybe<Scalars['Float']>
+  snapshotTime?: Maybe<Scalars['DateTime']>
   defaultInviteCount?: Maybe<Scalars['Float']>
   membershipPrice?: Maybe<Scalars['BigInt']>
-  referralCut?: Maybe<Scalars['BigInt']>
+  referralCut?: Maybe<Scalars['Float']>
   invitedInitialBalance?: Maybe<Scalars['BigInt']>
 }
 
-export type MembershipSystemWhereInput = {
+export type MembershipSystemSnapshotWhereInput = {
   id_eq?: Maybe<Scalars['ID']>
   id_in?: Maybe<Array<Scalars['ID']>>
   createdAt_eq?: Maybe<Scalars['DateTime']>
@@ -1613,6 +1616,17 @@ export type MembershipSystemWhereInput = {
   deletedAt_gte?: Maybe<Scalars['DateTime']>
   deletedById_eq?: Maybe<Scalars['ID']>
   deletedById_in?: Maybe<Array<Scalars['ID']>>
+  snapshotBlock_eq?: Maybe<Scalars['Int']>
+  snapshotBlock_gt?: Maybe<Scalars['Int']>
+  snapshotBlock_gte?: Maybe<Scalars['Int']>
+  snapshotBlock_lt?: Maybe<Scalars['Int']>
+  snapshotBlock_lte?: Maybe<Scalars['Int']>
+  snapshotBlock_in?: Maybe<Array<Scalars['Int']>>
+  snapshotTime_eq?: Maybe<Scalars['DateTime']>
+  snapshotTime_lt?: Maybe<Scalars['DateTime']>
+  snapshotTime_lte?: Maybe<Scalars['DateTime']>
+  snapshotTime_gt?: Maybe<Scalars['DateTime']>
+  snapshotTime_gte?: Maybe<Scalars['DateTime']>
   defaultInviteCount_eq?: Maybe<Scalars['Int']>
   defaultInviteCount_gt?: Maybe<Scalars['Int']>
   defaultInviteCount_gte?: Maybe<Scalars['Int']>
@@ -1625,12 +1639,12 @@ export type MembershipSystemWhereInput = {
   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']>>
+  referralCut_eq?: Maybe<Scalars['Int']>
+  referralCut_gt?: Maybe<Scalars['Int']>
+  referralCut_gte?: Maybe<Scalars['Int']>
+  referralCut_lt?: Maybe<Scalars['Int']>
+  referralCut_lte?: Maybe<Scalars['Int']>
+  referralCut_in?: Maybe<Array<Scalars['Int']>>
   invitedInitialBalance_eq?: Maybe<Scalars['BigInt']>
   invitedInitialBalance_gt?: Maybe<Scalars['BigInt']>
   invitedInitialBalance_gte?: Maybe<Scalars['BigInt']>
@@ -1639,7 +1653,7 @@ export type MembershipSystemWhereInput = {
   invitedInitialBalance_in?: Maybe<Array<Scalars['BigInt']>>
 }
 
-export type MembershipSystemWhereUniqueInput = {
+export type MembershipSystemSnapshotWhereUniqueInput = {
   id: Scalars['ID']
 }
 
@@ -1891,9 +1905,9 @@ export type Query = {
   membershipPriceUpdatedEvents: Array<MembershipPriceUpdatedEvent>
   membershipPriceUpdatedEventByUniqueInput?: Maybe<MembershipPriceUpdatedEvent>
   membershipPriceUpdatedEventsConnection: MembershipPriceUpdatedEventConnection
-  membershipSystems: Array<MembershipSystem>
-  membershipSystemByUniqueInput?: Maybe<MembershipSystem>
-  membershipSystemsConnection: MembershipSystemConnection
+  membershipSystemSnapshots: Array<MembershipSystemSnapshot>
+  membershipSystemSnapshotByUniqueInput?: Maybe<MembershipSystemSnapshot>
+  membershipSystemSnapshotsConnection: MembershipSystemSnapshotConnection
   memberships: Array<Membership>
   membershipByUniqueInput?: Maybe<Membership>
   membershipsConnection: MembershipConnection
@@ -2172,24 +2186,24 @@ export type QueryMembershipPriceUpdatedEventsConnectionArgs = {
   orderBy?: Maybe<MembershipPriceUpdatedEventOrderByInput>
 }
 
-export type QueryMembershipSystemsArgs = {
+export type QueryMembershipSystemSnapshotsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
-  where?: Maybe<MembershipSystemWhereInput>
-  orderBy?: Maybe<MembershipSystemOrderByInput>
+  where?: Maybe<MembershipSystemSnapshotWhereInput>
+  orderBy?: Maybe<MembershipSystemSnapshotOrderByInput>
 }
 
-export type QueryMembershipSystemByUniqueInputArgs = {
-  where: MembershipSystemWhereUniqueInput
+export type QueryMembershipSystemSnapshotByUniqueInputArgs = {
+  where: MembershipSystemSnapshotWhereUniqueInput
 }
 
-export type QueryMembershipSystemsConnectionArgs = {
+export type QueryMembershipSystemSnapshotsConnectionArgs = {
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   last?: Maybe<Scalars['Int']>
   before?: Maybe<Scalars['String']>
-  where?: Maybe<MembershipSystemWhereInput>
-  orderBy?: Maybe<MembershipSystemOrderByInput>
+  where?: Maybe<MembershipSystemSnapshotWhereInput>
+  orderBy?: Maybe<MembershipSystemSnapshotOrderByInput>
 }
 
 export type QueryMembershipsArgs = {
@@ -2314,7 +2328,7 @@ export type ReferralCutUpdatedEvent = BaseGraphQlObject & {
   member: Membership
   memberId: Scalars['String']
   /** New cut value. */
-  newValue: Scalars['BigInt']
+  newValue: Scalars['Int']
 }
 
 export type ReferralCutUpdatedEventConnection = {
@@ -2327,7 +2341,7 @@ export type ReferralCutUpdatedEventConnection = {
 export type ReferralCutUpdatedEventCreateInput = {
   eventId: Scalars['ID']
   memberId: Scalars['ID']
-  newValue: Scalars['BigInt']
+  newValue: Scalars['Float']
 }
 
 export type ReferralCutUpdatedEventEdge = {
@@ -2354,7 +2368,7 @@ export enum ReferralCutUpdatedEventOrderByInput {
 export type ReferralCutUpdatedEventUpdateInput = {
   eventId?: Maybe<Scalars['ID']>
   memberId?: Maybe<Scalars['ID']>
-  newValue?: Maybe<Scalars['BigInt']>
+  newValue?: Maybe<Scalars['Float']>
 }
 
 export type ReferralCutUpdatedEventWhereInput = {
@@ -2386,12 +2400,12 @@ export type ReferralCutUpdatedEventWhereInput = {
   eventId_in?: Maybe<Array<Scalars['ID']>>
   memberId_eq?: Maybe<Scalars['ID']>
   memberId_in?: Maybe<Array<Scalars['ID']>>
-  newValue_eq?: Maybe<Scalars['BigInt']>
-  newValue_gt?: Maybe<Scalars['BigInt']>
-  newValue_gte?: Maybe<Scalars['BigInt']>
-  newValue_lt?: Maybe<Scalars['BigInt']>
-  newValue_lte?: Maybe<Scalars['BigInt']>
-  newValue_in?: Maybe<Array<Scalars['BigInt']>>
+  newValue_eq?: Maybe<Scalars['Int']>
+  newValue_gt?: Maybe<Scalars['Int']>
+  newValue_gte?: Maybe<Scalars['Int']>
+  newValue_lt?: Maybe<Scalars['Int']>
+  newValue_lte?: Maybe<Scalars['Int']>
+  newValue_in?: Maybe<Array<Scalars['Int']>>
 }
 
 export type ReferralCutUpdatedEventWhereUniqueInput = {

+ 158 - 3
tests/integration-tests/src/fixtures/membershipModule.ts

@@ -19,12 +19,13 @@ import {
   StakingAccountConfirmedEvent,
   StakingAccountRemovedEvent,
   Event,
+  MembershipSystemSnapshot,
 } from '../QueryNodeApiSchema.generated'
 import { blake2AsHex } from '@polkadot/util-crypto'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { CreateInterface, createType } from '@joystream/types'
 import { MembershipMetadata } from '@joystream/metadata-protobuf'
-import { EventDetails, MemberInvitedEventDetails, MembershipBoughtEventDetails } from '../types'
+import { EventDetails, MemberInvitedEventDetails, MembershipBoughtEventDetails, MembershipEventName } from '../types'
 
 // FIXME: Retrieve from runtime when possible!
 const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
@@ -212,8 +213,9 @@ export class BuyMembershipWithInsufficienFundsFixture extends MembershipFixture
 
     const balance = await this.api.getBalance(this.account)
 
-    assert(
-      balance.toBn() < membershipFee.add(membershipTransactionFee),
+    assert.isBelow(
+      balance.toNumber(),
+      membershipFee.add(membershipTransactionFee).toNumber(),
       'Account already has sufficient balance to purchase membership'
     )
 
@@ -722,3 +724,156 @@ export class RemoveStakingAccountsHappyCaseFixture extends MembershipFixture {
     )
   }
 }
+
+type MembershipSystemValues = {
+  referralCut: number
+  defaultInviteCount: number
+  membershipPrice: BN
+  invitedInitialBalance: BN
+}
+
+export class SudoUpdateMembershipSystem extends MembershipFixture {
+  private query: QueryNodeApi
+  private newValues: Partial<MembershipSystemValues>
+
+  public constructor(api: Api, query: QueryNodeApi, newValues: Partial<MembershipSystemValues>) {
+    super(api)
+    this.query = query
+    this.newValues = newValues
+  }
+
+  private async getMembershipSystemValuesAt(blockNumber: number): Promise<MembershipSystemValues> {
+    const blockHash = await this.api.getBlockHash(blockNumber)
+    return {
+      referralCut: (await this.api.query.members.referralCut.at(blockHash)).toNumber(),
+      defaultInviteCount: (await this.api.query.members.initialInvitationCount.at(blockHash)).toNumber(),
+      invitedInitialBalance: await this.api.query.members.initialInvitationBalance.at(blockHash),
+      membershipPrice: await this.api.query.members.membershipPrice.at(blockHash),
+    }
+  }
+
+  private async assertBeforeSnapshotIsValid(beforeSnapshot: MembershipSystemSnapshot) {
+    assert.isNumber(beforeSnapshot.snapshotBlock)
+    const chainValues = await this.getMembershipSystemValuesAt(beforeSnapshot.snapshotBlock)
+    assert.equal(beforeSnapshot.referralCut, chainValues.referralCut)
+    assert.equal(beforeSnapshot.invitedInitialBalance, chainValues.invitedInitialBalance.toString())
+    assert.equal(beforeSnapshot.membershipPrice, chainValues.membershipPrice.toString())
+    assert.equal(beforeSnapshot.defaultInviteCount, chainValues.defaultInviteCount)
+  }
+
+  private assertAfterSnapshotIsValid(
+    beforeSnapshot: MembershipSystemSnapshot,
+    afterSnapshot: MembershipSystemSnapshot
+  ) {
+    const { newValues } = this
+    const expectedValue = (field: keyof MembershipSystemValues) => {
+      const newValue = newValues[field]
+      return newValue === undefined ? beforeSnapshot[field] : newValue instanceof BN ? newValue.toString() : newValue
+    }
+    assert.equal(afterSnapshot.referralCut, expectedValue('referralCut'))
+    assert.equal(afterSnapshot.invitedInitialBalance, expectedValue('invitedInitialBalance'))
+    assert.equal(afterSnapshot.membershipPrice, expectedValue('membershipPrice'))
+    assert.equal(afterSnapshot.defaultInviteCount, expectedValue('defaultInviteCount'))
+  }
+
+  private checkEvent<T extends AnyQueryNodeEvent>(qEvent: T | undefined, txHash: string): T {
+    if (!qEvent) {
+      throw new Error('Missing query-node event')
+    }
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    return qEvent
+  }
+
+  async execute(): Promise<void> {
+    const extrinsics: { tx: SubmittableExtrinsic<'promise'>; eventName: MembershipEventName }[] = []
+    if (this.newValues.referralCut !== undefined) {
+      extrinsics.push({
+        tx: this.api.tx.sudo.sudo(this.api.tx.members.setReferralCut(this.newValues.referralCut)),
+        eventName: 'ReferralCutUpdated',
+      })
+    }
+    if (this.newValues.defaultInviteCount !== undefined) {
+      extrinsics.push({
+        tx: this.api.tx.sudo.sudo(this.api.tx.members.setInitialInvitationCount(this.newValues.defaultInviteCount)),
+        eventName: 'InitialInvitationCountUpdated',
+      })
+    }
+    if (this.newValues.membershipPrice !== undefined) {
+      extrinsics.push({
+        tx: this.api.tx.sudo.sudo(this.api.tx.members.setMembershipPrice(this.newValues.membershipPrice)),
+        eventName: 'MembershipPriceUpdated',
+      })
+    }
+    if (this.newValues.invitedInitialBalance !== undefined) {
+      extrinsics.push({
+        tx: this.api.tx.sudo.sudo(
+          this.api.tx.members.setInitialInvitationBalance(this.newValues.invitedInitialBalance)
+        ),
+        eventName: 'InitialInvitationBalanceUpdated',
+      })
+    }
+
+    // We don't use api.makeSudoCall, since we cannot(?) then access tx hashes
+    const sudo = await this.api.query.sudo.key()
+    const results = await Promise.all(extrinsics.map(({ tx }) => this.api.signAndSend(tx, sudo)))
+    const events = await Promise.all(
+      results.map((r, i) => this.api.retrieveMembershipEventDetails(r, extrinsics[i].eventName))
+    )
+
+    const beforeSnapshotMaxBlockNumber = Math.min(...events.map((e) => e.blockNumber)) - 1
+    const afterSnapshotBlockNumber = Math.max(...events.map((e) => e.blockNumber))
+
+    // Fetch "afterSnapshot" first to make sure query node has progressed enough
+    const afterSnapshot = (await this.query.tryQueryWithTimeout(
+      () => this.query.getMembershipSystemSnapshot(afterSnapshotBlockNumber),
+      (snapshot) => assert.isOk(snapshot)
+    )) as MembershipSystemSnapshot
+
+    const beforeSnapshot = await this.query.getMembershipSystemSnapshot(beforeSnapshotMaxBlockNumber, 'lte')
+
+    if (!beforeSnapshot) {
+      throw new Error(`MembershipSystemSnapshot before block ${beforeSnapshotMaxBlockNumber} not found!`)
+    }
+
+    // Validate snapshots
+    await this.assertBeforeSnapshotIsValid(beforeSnapshot)
+    this.assertAfterSnapshotIsValid(beforeSnapshot, afterSnapshot)
+
+    // Check events
+    await Promise.all(
+      events.map(async (event, i) => {
+        const { eventName, tx } = extrinsics[i]
+        const txHash = tx.hash.toString()
+        const { blockNumber, indexInBlock } = event
+        if (eventName === 'ReferralCutUpdated') {
+          const { newValue } = this.checkEvent(
+            await this.query.getReferralCutUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newValue, this.newValues.referralCut)
+        }
+        if (eventName === 'MembershipPriceUpdated') {
+          const { newPrice } = this.checkEvent(
+            await this.query.getMembershipPriceUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newPrice, this.newValues.membershipPrice!.toString())
+        }
+        if (eventName === 'InitialInvitationBalanceUpdated') {
+          const { newInitialBalance } = this.checkEvent(
+            await this.query.getInitialInvitationBalanceUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newInitialBalance, this.newValues.invitedInitialBalance!.toString())
+        }
+        if (eventName === 'InitialInvitationCountUpdated') {
+          const { newInitialInvitationCount } = this.checkEvent(
+            await this.query.getInitialInvitationCountUpdatedEvent(blockNumber, indexInBlock),
+            txHash
+          )
+          assert.equal(newInitialInvitationCount, this.newValues.defaultInviteCount)
+        }
+      })
+    )
+  }
+}

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

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

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

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

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

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

+ 39 - 0
tests/integration-tests/src/flows/membership/membershipSystem.ts

@@ -0,0 +1,39 @@
+import { FlowProps } from '../../Flow'
+import { SudoUpdateMembershipSystem } from '../../fixtures/membershipModule'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import BN from 'bn.js'
+
+export default async function membershipSystem({ api, query, env }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:membership-system')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const updates = [
+    {
+      defaultInviteCount: 10,
+      membershipPrice: new BN(1000),
+      referralCut: 5,
+      invitedInitialBalance: new BN(500),
+    },
+    {
+      defaultInviteCount: 5,
+      membershipPrice: new BN(500),
+    },
+    {
+      referralCut: 0,
+      invitedInitialBalance: new BN(100),
+    },
+  ]
+
+  const fixtures = updates.map((u) => new SudoUpdateMembershipSystem(api, query, u))
+  // Fixtures should be executed one-by-one to not interfere with each other (before->after snapshot checks)
+  for (const key in fixtures) {
+    const fixture = fixtures[key]
+    debug(`Running update fixture number ${key + 1}`)
+    await new FixtureRunner(fixture).run()
+  }
+
+  debug('Done')
+}

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

@@ -4,7 +4,7 @@ import { BuyMembershipHappyCaseFixture, TransferInvitesHappyCaseFixture } from '
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 
-export default async function membershipCreation({ api, query, env }: FlowProps): Promise<void> {
+export default async function transferringInvites({ api, query, env }: FlowProps): Promise<void> {
   const debug = Debugger('flow:transferring-invites')
   debug('Started')
   api.enableDebugTxLogs()

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

@@ -4,7 +4,7 @@ import { BuyMembershipHappyCaseFixture, UpdateAccountsHappyCaseFixture } from '.
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 
-export default async function profileUpdate({ api, query }: FlowProps): Promise<void> {
+export default async function updatingAccounts({ api, query }: FlowProps): Promise<void> {
   const debug = Debugger('flow:member-accounts-update')
   debug('Started')
   api.enableDebugTxLogs()

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

@@ -4,7 +4,7 @@ import { BuyMembershipHappyCaseFixture, UpdateProfileHappyCaseFixture } from '..
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 
-export default async function profileUpdate({ api, query }: FlowProps): Promise<void> {
+export default async function updatingProfile({ api, query }: FlowProps): Promise<void> {
   const debug = Debugger('flow:member-profile-update')
   debug('Started')
   api.enableDebugTxLogs()

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

@@ -4,13 +4,16 @@ 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 membershipSystem from '../flows/membership/membershipSystem'
 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)
+  const membershipSystemJob = job('membership system', membershipSystem)
+  // All other job should be executed after, otherwise changing membershipPrice etc. may break them
+  job('creating members', creatingMemberships).after(membershipSystemJob)
+  job('updating member profile', updatingMemberProfile).after(membershipSystemJob)
+  job('updating member accounts', updatingMemberAccounts).after(membershipSystemJob)
+  job('inviting members', invitingMebers).after(membershipSystemJob)
+  job('transferring invites', transferringInvites).after(membershipSystemJob)
+  job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
 })