Browse Source

Membership mappings - events part 1

Leszek Wiesner 4 years ago
parent
commit
dffaf3ad48

+ 6 - 0
query-node/manifest.yml

@@ -18,12 +18,14 @@ typegen:
     - members.MemberVerificationStatusUpdated
     - members.InvitesTransferred
     - members.MemberInvited
+    - members.StakingAccountAdded
     - members.StakingAccountConfirmed
     - members.StakingAccountRemoved
     - members.InitialInvitationCountUpdated
     - members.MembershipPriceUpdated
     - members.ReferralCutUpdated
     - members.InitialInvitationBalanceUpdated
+    - members.LeaderInvitationQuotaUpdated
   calls:
     - members.updateProfile
     - members.updateAccounts
@@ -54,6 +56,8 @@ mappings:
       handler: members_InvitesTransferred(DatabaseManager, SubstrateEvent)
     - event: members.MemberInvited
       handler: members_MemberInvited(DatabaseManager, SubstrateEvent)
+    - event: members.StakingAccountAdded
+      handler: members_StakingAccountAdded(DatabaseManager, SubstrateEvent)
     - event: members.StakingAccountConfirmed
       handler: members_StakingAccountConfirmed(DatabaseManager, SubstrateEvent)
     - event: members.StakingAccountRemoved
@@ -66,6 +70,8 @@ mappings:
       handler: members_ReferralCutUpdated(DatabaseManager, SubstrateEvent)
     - event: members.InitialInvitationBalanceUpdated
       handler: members_InitialInvitationBalanceUpdated(DatabaseManager, SubstrateEvent)
+    - event: members.LeaderInvitationQuotaUpdated
+      handler: members_LeaderInvitationQuotaUpdated(DatabaseManager, SubstrateEvent)
   extrinsicHandlers:
     # infer defaults here
     #- extrinsic: Balances.Transfer

+ 12 - 17
query-node/mappings/common.ts

@@ -1,20 +1,15 @@
+/*
+eslint-disable @typescript-eslint/naming-convention
+*/
 import { SubstrateEvent } from '@dzlzv/hydra-common'
-import { DatabaseManager } from '@dzlzv/hydra-db-utils'
-import { Block } from 'query-node/dist/src/modules/block/block.model'
-import { Network } from 'query-node/dist/src/modules/enums/enums'
-
-const currentNetwork = Network.OLYMPIA
-
-export async function prepareBlock(db: DatabaseManager, event: SubstrateEvent): Promise<Block> {
-  const block = await db.get(Block, { where: { block: event.blockNumber } })
-
-  if (block) {
-    return block
-  }
-
-  return new Block({
-    block: event.blockNumber,
-    executedAt: new Date(event.blockTimestamp.toNumber()),
-    network: currentNetwork,
+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 {
+  return new Event({
+    inBlock: event_.blockNumber,
+    inExtrinsic: event_.extrinsic?.hash,
+    indexInBlock: event_.index,
+    type,
   })
 }

+ 204 - 15
query-node/mappings/mappings.ts

@@ -3,18 +3,34 @@ eslint-disable @typescript-eslint/naming-convention
 */
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
-import { Membership, MembershipEntryMethod } from 'query-node/dist/src/modules/membership/membership.model'
+import { Membership } 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 { EventType, MembershipEntryMethod } from 'query-node/dist/src/modules/enums/enums'
 import { MembershipSystem } from 'query-node/dist/src/modules/membership-system/membership-system.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'
+import { MemberAccountsUpdatedEvent } from 'query-node/dist/src/modules/member-accounts-updated-event/member-accounts-updated-event.model'
+import { MemberInvitedEvent } from 'query-node/dist/src/modules/member-invited-event/member-invited-event.model'
 import { MemberId, BuyMembershipParameters, InviteMembershipParameters } from '@joystream/types/augment/all'
 import { MembershipMetadata } from '@joystream/metadata-protobuf'
+import { Event } from 'query-node/dist/src/modules/event/event.model'
+import { MemberVerificationStatusUpdatedEvent } from 'query-node/dist/src/modules/member-verification-status-updated-event/member-verification-status-updated-event.model'
+import { createEvent } from './common'
+import { InvitesTransferredEvent } from 'query-node/dist/src/modules/invites-transferred-event/invites-transferred-event.model'
+import { StakingAccountConfirmedEvent } from 'query-node/dist/src/modules/staking-account-confirmed-event/staking-account-confirmed-event.model'
+import { StakingAccountRemovedEvent } from 'query-node/dist/src/modules/staking-account-removed-event/staking-account-removed-event.model'
+import { InitialInvitationCountUpdatedEvent } from 'query-node/dist/src/modules/initial-invitation-count-updated-event/initial-invitation-count-updated-event.model'
+import { MembershipPriceUpdatedEvent } from 'query-node/dist/src/modules/membership-price-updated-event/membership-price-updated-event.model'
+import { ReferralCutUpdatedEvent } from 'query-node/dist/src/modules/referral-cut-updated-event/referral-cut-updated-event.model'
+import { InitialInvitationBalanceUpdatedEvent } from 'query-node/dist/src/modules/initial-invitation-balance-updated-event/initial-invitation-balance-updated-event.model'
+import { StakingAccountAddedEvent } from 'query-node/dist/src/modules/staking-account-added-event/staking-account-added-event.model'
+import { LeaderInvitationQuotaUpdatedEvent } from 'query-node/dist/src/modules/leader-invitation-quota-updated-event/leader-invitation-quota-updated-event.model'
 
 async function getMemberById(db: DatabaseManager, id: MemberId): Promise<Membership> {
-  const member = await db.get(Membership, { where: { id: id.toString() } })
+  const member = await db.get(Membership, { where: { id: id.toString() }, relations: ['metadata'] })
   if (!member) {
     throw new Error(`Member(${id}) not found`)
   }
@@ -48,20 +64,25 @@ async function newMembershipFromParams(
   memberId: MemberId,
   entryMethod: MembershipEntryMethod,
   params: BuyMembershipParameters | InviteMembershipParameters
-): Promise<void> {
+): Promise<Membership> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   const membershipSystem = await getMembershipSystem(db)
   const { root_account: rootAccount, controller_account: controllerAccount, handle, metadata: metatadaBytes } = params
   const metadata = deserializeMemberMeta(metatadaBytes)
+
+  const metadataEntity = new MemberMetadata({
+    name: metadata?.getName(),
+    about: metadata?.getAbout(),
+    avatarUri: metadata?.getAvatarUri(),
+  })
+
   const member = new Membership({
     id: memberId.toString(),
-    name: metadata?.getName(),
     rootAccount: rootAccount.toString(),
     controllerAccount: controllerAccount.toString(),
     handle: handle.unwrap().toString(),
-    about: metadata?.getAbout(),
-    avatarUri: metadata?.getAvatarUri(),
-    registeredAtBlock: await prepareBlock(db, event_),
+    metadata: metadataEntity,
+    registeredAtBlock: event_.blockNumber,
     registeredAtTime: new Date(event_.blockTimestamp.toNumber()),
     entry: entryMethod,
     referredBy:
@@ -79,13 +100,37 @@ async function newMembershipFromParams(
         : undefined,
   })
 
-  await db.save<Block>(member.registeredAtBlock)
+  await db.save<MemberMetadata>(member.metadata)
   await db.save<Membership>(member)
+
+  return 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)
+  const member = await newMembershipFromParams(
+    db,
+    event_,
+    memberId,
+    MembershipEntryMethod.PAID,
+    buyMembershipParameters
+  )
+  const membershipBoughtEvent = new MembershipBoughtEvent({
+    event: createEvent(event_, EventType.MembershipBought),
+    newMember: member,
+    controllerAccount: member.controllerAccount,
+    rootAccount: member.rootAccount,
+    handle: member.handle,
+    metadata: new MemberMetadata({
+      ...member.metadata,
+      id: undefined,
+    }),
+    referrer: member.referredBy,
+  })
+
+  await db.save<Event>(membershipBoughtEvent.event)
+  await db.save<MemberMetadata>(membershipBoughtEvent.metadata)
+  await db.save<MembershipBoughtEvent>(membershipBoughtEvent)
 }
 
 export async function members_MemberProfileUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -94,19 +139,34 @@ export async function members_MemberProfileUpdated(db: DatabaseManager, event_:
   const metadata = metadataBytesOpt.isSome ? deserializeMemberMeta(metadataBytesOpt.unwrap()) : undefined
   const member = await getMemberById(db, memberId)
   if (metadata?.hasName()) {
-    member.name = metadata.getName()
+    member.metadata.name = metadata.getName()
   }
   if (metadata?.hasAbout()) {
-    member.about = metadata.getAbout()
+    member.metadata.about = metadata.getAbout()
   }
   if (metadata?.hasAvatarUri()) {
-    member.avatarUri = metadata.getAvatarUri()
+    member.metadata.avatarUri = metadata.getAvatarUri()
   }
   if (handle.isSome) {
     member.handle = bytesToString(handle.unwrap())
   }
 
+  await db.save<MemberMetadata>(member.metadata)
   await db.save<Membership>(member)
+
+  const memberProfileUpdatedEvent = new MemberProfileUpdatedEvent({
+    event: createEvent(event_, EventType.MemberProfileUpdated),
+    member: member,
+    newHandle: member.handle,
+    newMetadata: new MemberMetadata({
+      ...member.metadata,
+      id: undefined,
+    }),
+  })
+
+  await db.save<Event>(memberProfileUpdatedEvent.event)
+  await db.save<MemberMetadata>(memberProfileUpdatedEvent.newMetadata)
+  await db.save<MemberProfileUpdatedEvent>(memberProfileUpdatedEvent)
 }
 
 export async function members_MemberAccountsUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -121,6 +181,16 @@ export async function members_MemberAccountsUpdated(db: DatabaseManager, event_:
   }
 
   await db.save<Membership>(member)
+
+  const memberAccountsUpdatedEvent = new MemberAccountsUpdatedEvent({
+    event: createEvent(event_, EventType.MemberAccountsUpdated),
+    member: member,
+    newRootAccount: member.rootAccount,
+    newControllerAccount: member.controllerAccount,
+  })
+
+  await db.save<Event>(memberAccountsUpdatedEvent.event)
+  await db.save<MemberAccountsUpdatedEvent>(memberAccountsUpdatedEvent)
 }
 
 export async function members_MemberVerificationStatusUpdated(
@@ -132,6 +202,15 @@ export async function members_MemberVerificationStatusUpdated(
   member.isVerified = verificationStatus.valueOf()
 
   await db.save<Membership>(member)
+
+  const memberVerificationStatusUpdatedEvent = new MemberVerificationStatusUpdatedEvent({
+    event: createEvent(event_, EventType.MemberVerificationStatusUpdated),
+    member: member,
+    isVerified: member.isVerified,
+  })
+
+  await db.save<Event>(memberVerificationStatusUpdatedEvent.event)
+  await db.save<MemberVerificationStatusUpdatedEvent>(memberVerificationStatusUpdatedEvent)
 }
 
 export async function members_InvitesTransferred(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -146,16 +225,62 @@ export async function members_InvitesTransferred(db: DatabaseManager, event_: Su
 
   await db.save<Membership>(sourceMember)
   await db.save<Membership>(targetMember)
+
+  const invitesTransferredEvent = new InvitesTransferredEvent({
+    event: createEvent(event_, EventType.InvitesTransferred),
+    sourceMember,
+    targetMember,
+    numberOfInvites: numberOfInvites.toNumber(),
+  })
+
+  await db.save<Event>(invitesTransferredEvent.event)
+  await db.save<InvitesTransferredEvent>(invitesTransferredEvent)
 }
 
 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)
+  const invitedMember = 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)
+
+  const memberInvitedEvent = new MemberInvitedEvent({
+    event: createEvent(event_, EventType.MemberInvited),
+    invitingMember,
+    newMember: invitedMember,
+    handle: invitedMember.handle,
+    rootAccount: invitedMember.rootAccount,
+    controllerAccount: invitedMember.controllerAccount,
+    metadata: new MemberMetadata({
+      ...invitedMember.metadata,
+      id: undefined,
+    }),
+  })
+
+  await db.save<Event>(memberInvitedEvent.event)
+  await db.save<MemberMetadata>(memberInvitedEvent.metadata)
+  await db.save<MemberInvitedEvent>(memberInvitedEvent)
+}
+
+export async function members_StakingAccountAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { memberId, accountId } = new Members.StakingAccountAddedEvent(event_).data
+
+  const stakingAccountAddedEvent = new StakingAccountAddedEvent({
+    event: createEvent(event_, EventType.StakingAccountAddedEvent),
+    member: new Membership({ id: memberId.toString() }),
+    account: accountId.toString(),
+  })
+
+  await db.save<Event>(stakingAccountAddedEvent.event)
+  await db.save<StakingAccountAddedEvent>(stakingAccountAddedEvent)
 }
 
 export async function members_StakingAccountConfirmed(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -164,6 +289,15 @@ export async function members_StakingAccountConfirmed(db: DatabaseManager, event
   member.boundAccounts.push(accountId.toString())
 
   await db.save<Membership>(member)
+
+  const stakingAccountConfirmedEvent = new StakingAccountConfirmedEvent({
+    event: createEvent(event_, EventType.StakingAccountConfirmed),
+    member,
+    account: accountId.toString(),
+  })
+
+  await db.save<Event>(stakingAccountConfirmedEvent.event)
+  await db.save<StakingAccountConfirmedEvent>(stakingAccountConfirmedEvent)
 }
 
 export async function members_StakingAccountRemoved(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -175,6 +309,15 @@ export async function members_StakingAccountRemoved(db: DatabaseManager, event_:
   )
 
   await db.save<Membership>(member)
+
+  const stakingAccountRemovedEvent = new StakingAccountRemovedEvent({
+    event: createEvent(event_, EventType.StakingAccountRemoved),
+    member,
+    account: accountId.toString(),
+  })
+
+  await db.save<Event>(stakingAccountRemovedEvent.event)
+  await db.save<StakingAccountRemovedEvent>(stakingAccountRemovedEvent)
 }
 
 export async function members_InitialInvitationCountUpdated(
@@ -186,6 +329,14 @@ export async function members_InitialInvitationCountUpdated(
   membershipSystem.defaultInviteCount = newDefaultInviteCount.toNumber()
 
   await db.save<MembershipSystem>(membershipSystem)
+
+  const initialInvitationCountUpdatedEvent = new InitialInvitationCountUpdatedEvent({
+    event: createEvent(event_, EventType.InitialInvitationCountUpdated),
+    newInitialInvitationCount: newDefaultInviteCount.toNumber(),
+  })
+
+  await db.save<Event>(initialInvitationCountUpdatedEvent.event)
+  await db.save<InitialInvitationCountUpdatedEvent>(initialInvitationCountUpdatedEvent)
 }
 
 export async function members_MembershipPriceUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -194,6 +345,14 @@ export async function members_MembershipPriceUpdated(db: DatabaseManager, event_
   membershipSystem.membershipPrice = newMembershipPrice
 
   await db.save<MembershipSystem>(membershipSystem)
+
+  const membershipPriceUpdatedEvent = new MembershipPriceUpdatedEvent({
+    event: createEvent(event_, EventType.MembershipPriceUpdated),
+    newPrice: newMembershipPrice,
+  })
+
+  await db.save<Event>(membershipPriceUpdatedEvent.event)
+  await db.save<MembershipPriceUpdatedEvent>(membershipPriceUpdatedEvent)
 }
 
 export async function members_ReferralCutUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -202,6 +361,14 @@ export async function members_ReferralCutUpdated(db: DatabaseManager, event_: Su
   membershipSystem.referralCut = newReferralCut
 
   await db.save<MembershipSystem>(membershipSystem)
+
+  const referralCutUpdatedEvent = new ReferralCutUpdatedEvent({
+    event: createEvent(event_, EventType.ReferralCutUpdated),
+    newValue: newReferralCut,
+  })
+
+  await db.save<Event>(referralCutUpdatedEvent.event)
+  await db.save<ReferralCutUpdatedEvent>(referralCutUpdatedEvent)
 }
 
 export async function members_InitialInvitationBalanceUpdated(
@@ -213,4 +380,26 @@ export async function members_InitialInvitationBalanceUpdated(
   membershipSystem.invitedInitialBalance = newInvitedInitialBalance
 
   await db.save<MembershipSystem>(membershipSystem)
+
+  const initialInvitationBalanceUpdatedEvent = new InitialInvitationBalanceUpdatedEvent({
+    event: createEvent(event_, EventType.InitialInvitationBalanceUpdated),
+    newInitialBalance: newInvitedInitialBalance,
+  })
+
+  await db.save<Event>(initialInvitationBalanceUpdatedEvent.event)
+  await db.save<InitialInvitationBalanceUpdatedEvent>(initialInvitationBalanceUpdatedEvent)
+}
+
+export async function members_LeaderInvitationQuotaUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const { u32: newQuota } = new Members.LeaderInvitationQuotaUpdatedEvent(event_).data
+
+  const leaderInvitationQuotaUpdatedEvent = new LeaderInvitationQuotaUpdatedEvent({
+    event: createEvent(event_, EventType.LeaderInvitationQuotaUpdated),
+    newInvitationQuota: newQuota.toNumber(),
+  })
+
+  // TODO: Update MembershipSystem?
+
+  await db.save<Event>(leaderInvitationQuotaUpdatedEvent.event)
+  await db.save<LeaderInvitationQuotaUpdatedEvent>(leaderInvitationQuotaUpdatedEvent)
 }

+ 227 - 10
query-node/schema.graphql

@@ -19,6 +19,17 @@ enum MembershipEntryMethod {
   GENESIS
 }
 
+type MemberMetadata @entity {
+  "Member's name"
+  name: String
+
+  "A Url to member's Avatar image TODO: Storage asset"
+  avatarUri: String
+
+  "Short text chosen by member to share information about themselves"
+  about: String
+}
+
 "Stored information about a registered user"
 type Membership @entity {
   "MemberId: runtime identifier for a user"
@@ -27,14 +38,8 @@ type Membership @entity {
   "The unique handle chosen by member"
   handle: String! @unique @fulltext(query: "membersByHandle")
 
-  "Member's name"
-  name: String
-
-  "A Url to member's Avatar image"
-  avatarUri: String
-
-  "Short text chosen by member to share information about themselves"
-  about: String
+  "Member's metadata"
+  metadata: MemberMetadata!
 
   "Member's controller account id"
   controllerAccount: String!
@@ -42,8 +47,8 @@ type Membership @entity {
   "Member's root account id"
   rootAccount: String!
 
-  "Blocknumber when member was registered"
-  registeredAtBlock: Block!
+  "Block number when member was registered"
+  registeredAtBlock: Int!
 
   "Timestamp when member was registered"
   registeredAtTime: DateTime!
@@ -86,3 +91,215 @@ type MembershipSystem @entity {
   "The initial, locked, balance credited to controller account of invitee."
   invitedInitialBalance: BigInt!
 }
+
+# Membership-related events
+
+enum EventType {
+  MembershipBought,
+  MemberInvited,
+  MemberProfileUpdated,
+  MemberAccountsUpdated,
+  MemberVerificationStatusUpdated,
+  ReferralCutUpdated,
+  InvitesTransferred,
+  MembershipPriceUpdated,
+  InitialInvitationBalanceUpdated,
+  LeaderInvitationQuotaUpdated,
+  InitialInvitationCountUpdated,
+  StakingAccountAddedEvent,
+  StakingAccountConfirmed,
+  StakingAccountRemoved,
+}
+
+type Event @entity {
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of a block in which the event was emitted."
+  inBlock: Int!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  "Type of the event"
+  type: EventType!
+}
+
+
+type MembershipBoughtEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "New membership created."
+  newMember: Membership!
+
+  "New member root account in SS58 encoding."
+  rootAccount: String!
+
+  "New member controller in SS58 encoding."
+  controllerAccount: String!
+
+  "New member handle."
+  handle: String!
+
+  "New member metadata"
+  metadata: MemberMetadata!
+
+  "Referrer member."
+  referrer: Membership
+}
+
+type MemberInvitedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Inviting member."
+  invitingMember: Membership!
+
+  "New membership created."
+  newMember: Membership!
+
+  "New member root account in SS58 encoding."
+  rootAccount: String!
+
+  "New member controller in SS58 encoding."
+  controllerAccount: String!
+
+  "New member handle."
+  handle: String!
+
+  "New member metadata"
+  metadata: MemberMetadata!
+}
+
+type MemberProfileUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership being updated."
+  member: Membership!
+
+  "New member handle. Null means no new value was provided."
+  newHandle: String
+
+  "New member metadata. (empty values inside metadata mean no new value was provided)"
+  newMetadata: MemberMetadata!
+}
+
+type MemberAccountsUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  "New member root account in SS58 encoding. Null means no new value was provided."
+  newRootAccount: String
+
+  "New member controller in SS58 encoding. Null means no new value was provided."
+  newControllerAccount: String
+}
+
+type MemberVerificationStatusUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  #"TODO: Worker updating status"
+  #worker: Worker!
+
+  "New status."
+  isVerified: Boolean!
+}
+
+type ReferralCutUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  "New cut value."
+  newValue: BigInt!
+}
+
+type InvitesTransferredEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership sending invites."
+  sourceMember: Membership!
+
+  "Membership receiving invites."
+  targetMember: Membership!
+
+  "Number of invites transferred."
+  numberOfInvites: Int!
+}
+
+type MembershipPriceUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "The new membership price."
+  newPrice: BigInt!
+}
+
+type InitialInvitationBalanceUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "New initial invitation balance."
+  newInitialBalance: BigInt!
+}
+
+type LeaderInvitationQuotaUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "New quota."
+  newInvitationQuota: Int!
+}
+
+type InitialInvitationCountUpdatedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "New initial invitation count for members."
+  newInitialInvitationCount: Int!
+}
+
+type StakingAccountAddedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  "New staking account in SS58 encoding."
+  account: String!
+}
+
+type StakingAccountConfirmedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  "New staking account in SS58 encoding."
+  account: String!
+}
+
+type StakingAccountRemovedEvent @entity {
+  "Generic event data"
+  event: Event!
+
+  "Membership in question."
+  member: Membership!
+
+  "New staking account in SS58 encoding."
+  account: String!
+}

+ 47 - 8
tests/integration-tests/src/Api.ts

@@ -14,6 +14,7 @@ import { types } from '@joystream/types'
 import { v4 as uuid } from 'uuid'
 import Debugger from 'debug'
 import { DispatchError } from '@polkadot/types/interfaces/system'
+import { EventDetails, MemberInvitedEventDetails, MembershipBoughtEventDetails, MembershipEventName } from './types'
 
 export enum WorkingGroups {
   StorageWorkingGroup = 'storageWorkingGroup',
@@ -208,21 +209,59 @@ export class Api {
     return paymentInfo.partialFee
   }
 
+  // TODO: Augmentations comming with new @polkadot/typegen!
+
   public findEventRecord(events: EventRecord[], section: string, method: string): EventRecord | undefined {
     return events.find((record) => record.event.section === section && record.event.method === method)
   }
 
-  public findMemberBoughtEvent(events: EventRecord[]): MemberId | undefined {
-    const record = this.findEventRecord(events, 'members', 'MembershipBought')
-    if (record) {
-      return record.event.data[0] as MemberId
+  public async retrieveEventDetails(
+    result: ISubmittableResult,
+    section: string,
+    method: string
+  ): Promise<EventDetails | undefined> {
+    const { status, events } = result
+    const record = this.findEventRecord(events, section, method)
+    if (!record) {
+      return
+    }
+
+    const blockHash = status.asInBlock
+    const { number: blockNumber } = await this.api.rpc.chain.getHeader(blockHash)
+    const blockEvents = await this.api.query.system.events.at(blockHash)
+    const indexInBlock = blockEvents.findIndex(({ event: blockEvent }) => blockEvent.hash.eq(record.event.hash))
+
+    return {
+      event: record.event,
+      blockNumber: blockNumber.toNumber(),
+      indexInBlock,
+    }
+  }
+
+  public async retrieveMembershipEventDetails(
+    result: ISubmittableResult,
+    eventName: MembershipEventName
+  ): Promise<EventDetails> {
+    const details = await this.retrieveEventDetails(result, 'members', eventName)
+    if (!details) {
+      throw new Error(`${eventName} details not found in result: ${JSON.stringify(result.toHuman())}`)
+    }
+    return details
+  }
+
+  public async retrieveMembershipBoughtEventDetails(result: ISubmittableResult): Promise<MembershipBoughtEventDetails> {
+    const details = await this.retrieveMembershipEventDetails(result, 'MembershipBought')
+    return {
+      ...details,
+      memberId: details.event.data[0] as MemberId,
     }
   }
 
-  public findMemberInvitedEvent(events: EventRecord[]): MemberId | undefined {
-    const record = this.findEventRecord(events, 'members', 'MemberInvited')
-    if (record) {
-      return record.event.data[0] as MemberId
+  public async retrieveMemberInvitedEventDetails(result: ISubmittableResult): Promise<MemberInvitedEventDetails> {
+    const details = await this.retrieveMembershipEventDetails(result, 'MemberInvited')
+    return {
+      ...details,
+      newMemberId: details.event.data[0] as MemberId,
     }
   }
 

+ 267 - 12
tests/integration-tests/src/QueryNodeApi.ts

@@ -16,7 +16,7 @@ export class QueryNodeApi {
     query: () => Promise<QueryResultT>,
     assertResultIsValid: (res: QueryResultT) => void,
     timeoutMs = 120000,
-    retryTimeMs = 5000
+    retryTimeMs = 30000
   ): Promise<QueryResultT> {
     const retryDebug = Debugger('query-node-api:retry')
     return new Promise((resolve, reject) => {
@@ -34,7 +34,11 @@ export class QueryNodeApi {
               clearTimeout(timeout)
               resolve(result)
             } catch (e) {
-              retryDebug(`Unexpected query result, retyring query in ${retryTimeMs}ms...`)
+              retryDebug(
+                `Unexpected query result${
+                  e && e.message ? ` (${e.message})` : ''
+                }, retyring query in ${retryTimeMs}ms...`
+              )
               lastError = e
               setTimeout(tryQuery, retryTimeMs)
             }
@@ -50,22 +54,20 @@ export class QueryNodeApi {
     })
   }
 
-  public async getMemberById(id: MemberId): Promise<ApolloQueryResult<Pick<Query, 'membership'>>> {
+  public async getMemberById(id: MemberId): Promise<ApolloQueryResult<Pick<Query, 'membershipByUniqueInput'>>> {
     const MEMBER_BY_ID_QUERY = gql`
       query($id: ID!) {
-        membership(where: { id: $id }) {
+        membershipByUniqueInput(where: { id: $id }) {
           id
           handle
-          name
-          avatarUri
-          about
+          metadata {
+            name
+            avatarUri
+            about
+          }
           controllerAccount
           rootAccount
-          registeredAtBlock {
-            block
-            executedAt
-            network
-          }
+          registeredAtBlock
           registeredAtTime
           entry
           isVerified
@@ -85,4 +87,257 @@ export class QueryNodeApi {
 
     return this.queryNodeProvider.query({ query: MEMBER_BY_ID_QUERY, variables: { id: id.toNumber() } })
   }
+
+  public async getMembershipBoughtEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'membershipBoughtEvents'>>> {
+    const MEMBERTSHIP_BOUGHT_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        membershipBoughtEvents(where: { newMemberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          newMember {
+            id
+          }
+          rootAccount
+          controllerAccount
+          handle
+          metadata {
+            name
+            avatarUri
+            about
+          }
+          referrer {
+            id
+          }
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getMembershipBoughtEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: MEMBERTSHIP_BOUGHT_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getMemberProfileUpdatedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'memberProfileUpdatedEvents'>>> {
+    const MEMBER_PROFILE_UPDATED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        memberProfileUpdatedEvents(where: { memberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          member {
+            id
+          }
+          newHandle
+          newMetadata {
+            name
+            avatarUri
+            about
+          }
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getMemberProfileUpdatedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: MEMBER_PROFILE_UPDATED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getMemberAccountsUpdatedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'memberAccountsUpdatedEvents'>>> {
+    const MEMBER_ACCOUNTS_UPDATED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        memberAccountsUpdatedEvents(where: { memberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          member {
+            id
+          }
+          newRootAccount
+          newControllerAccount
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getMemberAccountsUpdatedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: MEMBER_ACCOUNTS_UPDATED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getMemberInvitedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'memberInvitedEvents'>>> {
+    const MEMBER_INVITED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        memberInvitedEvents(where: { newMemberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          invitingMember {
+            id
+          }
+          newMember {
+            id
+          }
+          rootAccount
+          controllerAccount
+          handle
+          metadata {
+            name
+            about
+            avatarUri
+          }
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getMemberInvitedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: MEMBER_INVITED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getInvitesTransferredEvents(
+    fromMemberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'invitesTransferredEvents'>>> {
+    const INVITES_TRANSFERRED_BY_MEMBER_ID = gql`
+      query($from: ID!) {
+        invitesTransferredEvents(where: { sourceMemberId_eq: $from }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          sourceMember {
+            id
+          }
+          targetMember {
+            id
+          }
+          numberOfInvites
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getInvitesTransferredEvents(${fromMemberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: INVITES_TRANSFERRED_BY_MEMBER_ID,
+      variables: { from: fromMemberId.toNumber() },
+    })
+  }
+
+  public async getStakingAccountAddedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'stakingAccountAddedEvents'>>> {
+    const STAKING_ACCOUNT_ADDED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        stakingAccountAddedEvents(where: { memberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          member {
+            id
+          }
+          account
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getStakingAccountAddedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: STAKING_ACCOUNT_ADDED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getStakingAccountConfirmedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'stakingAccountConfirmedEvents'>>> {
+    const STAKING_ACCOUNT_CONFIRMED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        stakingAccountConfirmedEvents(where: { memberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          member {
+            id
+          }
+          account
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getStakingAccountConfirmedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: STAKING_ACCOUNT_CONFIRMED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
+
+  public async getStakingAccountRemovedEvents(
+    memberId: MemberId
+  ): Promise<ApolloQueryResult<Pick<Query, 'stakingAccountRemovedEvents'>>> {
+    const STAKING_ACCOUNT_REMOVED_BY_MEMBER_ID = gql`
+      query($memberId: ID!) {
+        stakingAccountRemovedEvents(where: { memberId_eq: $memberId }) {
+          event {
+            inBlock
+            inExtrinsic
+            indexInBlock
+            type
+          }
+          member {
+            id
+          }
+          account
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getStakingAccountRemovedEvents(${memberId.toString()})`)
+
+    return this.queryNodeProvider.query({
+      query: STAKING_ACCOUNT_REMOVED_BY_MEMBER_ID,
+      variables: { memberId: memberId.toNumber() },
+    })
+  }
 }

File diff suppressed because it is too large
+ 1417 - 206
tests/integration-tests/src/QueryNodeApiSchema.generated.ts


+ 318 - 69
tests/integration-tests/src/fixtures/membershipModule.ts

@@ -4,14 +4,27 @@ import { assert } from 'chai'
 import { BaseFixture } from '../Fixture'
 import { MemberId } from '@joystream/types/common'
 import Debugger from 'debug'
-import { ISubmittableResult } from '@polkadot/types/types'
 import { QueryNodeApi } from '../QueryNodeApi'
 import { BuyMembershipParameters, Membership } from '@joystream/types/members'
-import { Membership as QueryNodeMembership, MembershipEntryMethod } from '../QueryNodeApiSchema.generated'
+import {
+  Membership as QueryNodeMembership,
+  MembershipEntryMethod,
+  MembershipBoughtEvent,
+  EventType,
+  MemberProfileUpdatedEvent,
+  MemberAccountsUpdatedEvent,
+  MemberInvitedEvent,
+  InvitesTransferredEvent,
+  StakingAccountAddedEvent,
+  StakingAccountConfirmedEvent,
+  StakingAccountRemovedEvent,
+  Event,
+} 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'
 
 // FIXME: Retrieve from runtime when possible!
 const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
@@ -20,6 +33,9 @@ type MemberContext = {
   account: string
   memberId: MemberId
 }
+
+type AnyQueryNodeEvent = { event: Event }
+
 // common code for fixtures
 abstract class MembershipFixture extends BaseFixture {
   generateParamsFromAccountId(accountId: string): CreateInterface<BuyMembershipParameters> {
@@ -39,10 +55,6 @@ abstract class MembershipFixture extends BaseFixture {
     return this.api.tx.members.buyMembership(this.generateParamsFromAccountId(accountId))
   }
 
-  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),
@@ -50,8 +62,13 @@ abstract class MembershipFixture extends BaseFixture {
     })
   }
 
-  sendInviteMemberTx(memberId: MemberId, inviterAccount: string, inviteeAccount: string): Promise<ISubmittableResult> {
-    return this.api.signAndSend(this.generateInviteMemberTx(memberId, inviteeAccount), inviterAccount)
+  findMatchingQueryNodeEvent<T extends AnyQueryNodeEvent>(eventToFind: EventDetails, queryNodeEvents: T[]) {
+    const { blockNumber, indexInBlock } = eventToFind
+    const qEvent = queryNodeEvents.find((e) => e.event.inBlock === blockNumber && e.event.indexInBlock === indexInBlock)
+    if (!qEvent) {
+      throw new Error(`Could not find matching query-node event (expected ${blockNumber}:${indexInBlock})!`)
+    }
+    return qEvent
   }
 }
 
@@ -78,9 +95,7 @@ export class BuyMembershipHappyCaseFixture extends MembershipFixture implements
       handle,
       rootAccount,
       controllerAccount,
-      name,
-      about,
-      avatarUri,
+      metadata: { name, about, avatarUri },
       isVerified,
       entry,
     } = qMember as QueryNodeMembership
@@ -97,6 +112,29 @@ export class BuyMembershipHappyCaseFixture extends MembershipFixture implements
     assert.equal(entry, MembershipEntryMethod.Paid)
   }
 
+  private assertEventMatchQueriedResult(
+    eventDetails: MembershipBoughtEventDetails,
+    account: string,
+    txHash: string,
+    qEvents: MembershipBoughtEvent[]
+  ) {
+    assert.equal(qEvents.length, 1, `Invalid number of MembershipBoughtEvents recieved`)
+    const [qEvent] = qEvents
+    const txParams = this.generateParamsFromAccountId(account)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(qEvent.event.inBlock, eventDetails.blockNumber)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
+    assert.equal(qEvent.event.type, EventType.MembershipBought)
+    assert.equal(qEvent.newMember.id, eventDetails.memberId.toString())
+    assert.equal(qEvent.handle, txParams.handle)
+    assert.equal(qEvent.rootAccount, txParams.root_account.toString())
+    assert.equal(qEvent.controllerAccount, txParams.controller_account.toString())
+    assert.equal(qEvent.metadata.name, metadata.getName())
+    assert.equal(qEvent.metadata.about, metadata.getAbout())
+    assert.equal(qEvent.metadata.avatarUri, metadata.getAvatarUri())
+  }
+
   async execute(): Promise<void> {
     // Fee estimation and transfer
     const membershipFee: BN = await this.api.getMembershipFee()
@@ -108,9 +146,10 @@ export class BuyMembershipHappyCaseFixture extends MembershipFixture implements
 
     await this.api.treasuryTransferBalanceToAccounts(this.accounts, estimatedFee)
 
-    this.memberIds = (await Promise.all(this.accounts.map((account) => this.sendBuyMembershipTx(account))))
-      .map(({ events }) => this.api.findMemberBoughtEvent(events))
-      .filter((id) => id !== undefined) as MemberId[]
+    const extrinsics = this.accounts.map((a) => this.generateBuyMembershipTx(a))
+    const results = await Promise.all(this.accounts.map((a, i) => this.api.signAndSend(extrinsics[i], a)))
+    const events = await Promise.all(results.map((r) => this.api.retrieveMembershipBoughtEventDetails(r)))
+    this.memberIds = events.map((e) => e.memberId)
 
     this.debug(`Registered ${this.memberIds.length} new members`)
 
@@ -127,14 +166,23 @@ export class BuyMembershipHappyCaseFixture extends MembershipFixture implements
     // Query-node part:
 
     // Ensure newly created members were parsed by query node
-    for (const i in members) {
-      const memberId = this.memberIds[i]
-      const member = members[i]
-      await this.query.tryQueryWithTimeout(
-        () => this.query.getMemberById(memberId),
-        (r) => this.assertMemberMatchQueriedResult(member, r.data.membership)
-      )
-    }
+    await Promise.all(
+      members.map(async (member, i) => {
+        const memberId = this.memberIds[i]
+        await this.query.tryQueryWithTimeout(
+          () => this.query.getMemberById(memberId),
+          (r) => this.assertMemberMatchQueriedResult(member, r.data.membershipByUniqueInput)
+        )
+        // Ensure the query node event is valid
+        const res = await this.query.getMembershipBoughtEvents(memberId)
+        this.assertEventMatchQueriedResult(
+          events[i],
+          this.accounts[i],
+          extrinsics[i].hash.toString(),
+          res.data.membershipBoughtEvents
+        )
+      })
+    )
   }
 }
 
@@ -169,7 +217,7 @@ export class BuyMembershipWithInsufficienFundsFixture extends MembershipFixture
       'Account already has sufficient balance to purchase membership'
     )
 
-    const result = await this.sendBuyMembershipTx(this.account)
+    const result = await this.api.signAndSend(this.generateBuyMembershipTx(this.account), this.account)
 
     this.expectDispatchError(result, 'Buying membership with insufficient funds should fail.')
 
@@ -178,7 +226,8 @@ export class BuyMembershipWithInsufficienFundsFixture extends MembershipFixture
   }
 }
 
-export class UpdateProfileHappyCaseFixture extends BaseFixture {
+// TODO: Add partial update to make sure it works too
+export class UpdateProfileHappyCaseFixture extends MembershipFixture {
   private query: QueryNodeApi
   private memberContext: MemberContext
   // Update data
@@ -195,13 +244,37 @@ export class UpdateProfileHappyCaseFixture extends BaseFixture {
 
   private assertProfileUpdateSuccesful(qMember?: QueryNodeMembership | null) {
     assert.isOk(qMember, 'Membership query result is empty')
-    const { name, handle, avatarUri, about } = qMember as QueryNodeMembership
+    const {
+      handle,
+      metadata: { name, avatarUri, about },
+    } = qMember as QueryNodeMembership
     assert.equal(name, this.newName)
     assert.equal(handle, this.newHandle)
     assert.equal(avatarUri, this.newAvatarUri)
     assert.equal(about, this.newAbout)
   }
 
+  private assertQueryNodeEventIsValid(
+    eventDetails: EventDetails,
+    txHash: string,
+    qEvents: MemberProfileUpdatedEvent[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    const {
+      event: { inExtrinsic, type },
+      member: { id: memberId },
+      newHandle,
+      newMetadata,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.MemberProfileUpdated)
+    assert.equal(memberId, this.memberContext.memberId.toString())
+    assert.equal(newHandle, this.newHandle)
+    assert.equal(newMetadata.name, this.newName)
+    assert.equal(newMetadata.about, this.newAbout)
+    assert.equal(newMetadata.avatarUri, this.newAvatarUri)
+  }
+
   async execute(): Promise<void> {
     const metadata = new MembershipMetadata()
     metadata.setName(this.newName)
@@ -214,15 +287,19 @@ export class UpdateProfileHappyCaseFixture extends BaseFixture {
     )
     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)
+    const txRes = await this.api.signAndSend(tx, this.memberContext.account)
+    const txHash = tx.hash.toString()
+    const updateEvent = await this.api.retrieveMembershipEventDetails(txRes, 'MemberProfileUpdated')
     await this.query.tryQueryWithTimeout(
       () => this.query.getMemberById(this.memberContext.memberId),
-      (res) => this.assertProfileUpdateSuccesful(res.data.membership)
+      (res) => this.assertProfileUpdateSuccesful(res.data.membershipByUniqueInput)
     )
+    const res = await this.query.getMemberProfileUpdatedEvents(this.memberContext.memberId)
+    this.assertQueryNodeEventIsValid(updateEvent, txHash, res.data.memberProfileUpdatedEvents)
   }
 }
 
-export class UpdateAccountsHappyCaseFixture extends BaseFixture {
+export class UpdateAccountsHappyCaseFixture extends MembershipFixture {
   private query: QueryNodeApi
   private memberContext: MemberContext
   // Update data
@@ -245,6 +322,25 @@ export class UpdateAccountsHappyCaseFixture extends BaseFixture {
     assert.equal(controllerAccount, this.newControllerAccount)
   }
 
+  private assertQueryNodeEventIsValid(
+    eventDetails: EventDetails,
+    txHash: string,
+    qEvents: MemberAccountsUpdatedEvent[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    const {
+      event: { inExtrinsic, type },
+      member: { id: memberId },
+      newControllerAccount,
+      newRootAccount,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.MemberAccountsUpdated)
+    assert.equal(memberId, this.memberContext.memberId.toString())
+    assert.equal(newControllerAccount, this.newControllerAccount)
+    assert.equal(newRootAccount, this.newRootAccount)
+  }
+
   async execute(): Promise<void> {
     const tx = this.api.tx.members.updateAccounts(
       this.memberContext.memberId,
@@ -253,11 +349,15 @@ export class UpdateAccountsHappyCaseFixture extends BaseFixture {
     )
     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)
+    const txRes = await this.api.signAndSend(tx, this.memberContext.account)
+    const txHash = tx.hash.toString()
+    const updateEvent = await this.api.retrieveMembershipEventDetails(txRes, 'MemberAccountsUpdated')
     await this.query.tryQueryWithTimeout(
       () => this.query.getMemberById(this.memberContext.memberId),
-      (res) => this.assertAccountsUpdateSuccesful(res.data.membership)
+      (res) => this.assertAccountsUpdateSuccesful(res.data.membershipByUniqueInput)
     )
+    const res = await this.query.getMemberAccountsUpdatedEvents(this.memberContext.memberId)
+    this.assertQueryNodeEventIsValid(updateEvent, txHash, res.data.memberAccountsUpdatedEvents)
   }
 }
 
@@ -279,9 +379,7 @@ export class InviteMembersHappyCaseFixture extends MembershipFixture {
       handle,
       rootAccount,
       controllerAccount,
-      name,
-      about,
-      avatarUri,
+      metadata: { name, about, avatarUri },
       isVerified,
       entry,
       invitedBy,
@@ -300,9 +398,33 @@ export class InviteMembersHappyCaseFixture extends MembershipFixture {
     assert.equal(invitedBy!.id, this.inviterContext.memberId.toString())
   }
 
+  private aseertQueryNodeEventIsValid(
+    eventDetails: MemberInvitedEventDetails,
+    account: string,
+    txHash: string,
+    qEvents: MemberInvitedEvent[]
+  ) {
+    assert.isNotEmpty(qEvents)
+    assert.equal(qEvents.length, 1, 'Unexpected number of MemberInvited events returned by query node')
+    const [qEvent] = qEvents
+    const txParams = this.generateParamsFromAccountId(account)
+    const metadata = MembershipMetadata.deserializeBinary(txParams.metadata.toU8a(true))
+    assert.equal(qEvent.event.inBlock, eventDetails.blockNumber)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.indexInBlock, eventDetails.indexInBlock)
+    assert.equal(qEvent.event.type, EventType.MemberInvited)
+    assert.equal(qEvent.newMember.id, eventDetails.newMemberId.toString())
+    assert.equal(qEvent.handle, txParams.handle)
+    assert.equal(qEvent.rootAccount, txParams.root_account)
+    assert.equal(qEvent.controllerAccount, txParams.controller_account)
+    assert.equal(qEvent.metadata.name, metadata.getName())
+    assert.equal(qEvent.metadata.about, metadata.getAbout())
+    assert.equal(qEvent.metadata.avatarUri, metadata.getAvatarUri())
+  }
+
   async execute(): Promise<void> {
-    const exampleTx = this.generateInviteMemberTx(this.inviterContext.memberId, this.accounts[0])
-    const feePerTx = await this.api.estimateTxFee(exampleTx, this.inviterContext.account)
+    const extrinsics = this.accounts.map((a) => this.generateInviteMemberTx(this.inviterContext.memberId, a))
+    const feePerTx = await this.api.estimateTxFee(extrinsics[0], this.inviterContext.account)
     await this.api.treasuryTransferBalance(this.inviterContext.account, feePerTx.muln(this.accounts.length))
 
     const initialInvitationBalance = await this.api.query.members.initialInvitationBalance()
@@ -313,28 +435,29 @@ export class InviteMembersHappyCaseFixture extends MembershipFixture {
 
     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[]
+    const txResults = await Promise.all(extrinsics.map((tx) => this.api.signAndSend(tx, this.inviterContext.account)))
+    const events = await Promise.all(txResults.map((res) => this.api.retrieveMemberInvitedEventDetails(res)))
+    const invitedMembersIds = events.map((e) => e.newMemberId)
 
     await Promise.all(
-      this.accounts.map((account, i) => {
+      this.accounts.map(async (account, i) => {
         const memberId = invitedMembersIds[i]
-        return this.query.tryQueryWithTimeout(
+        await this.query.tryQueryWithTimeout(
           () => this.query.getMemberById(memberId),
-          (res) => this.assertMemberCorrectlyInvited(account, res.data.membership)
+          (res) => this.assertMemberCorrectlyInvited(account, res.data.membershipByUniqueInput)
+        )
+        const res = await this.query.getMemberInvitedEvents(memberId)
+        this.aseertQueryNodeEventIsValid(
+          events[i],
+          account,
+          extrinsics[i].hash.toString(),
+          res.data.memberInvitedEvents
         )
       })
     )
 
     const {
-      data: { membership: inviter },
+      data: { membershipByUniqueInput: inviter },
     } = await this.query.getMemberById(this.inviterContext.memberId)
     assert.isOk(inviter)
     const { inviteCount, invitees } = inviter as QueryNodeMembership
@@ -369,6 +492,21 @@ export class TransferInvitesHappyCaseFixture extends MembershipFixture {
     this.invitesToTransfer = invitesToTransfer
   }
 
+  private assertQueryNodeEventIsValid(eventDetails: EventDetails, txHash: string, qEvents: InvitesTransferredEvent[]) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    const {
+      event: { inExtrinsic, type },
+      sourceMember,
+      targetMember,
+      numberOfInvites,
+    } = qEvent
+    assert.equal(inExtrinsic, txHash)
+    assert.equal(type, EventType.InvitesTransferred)
+    assert.equal(sourceMember.id, this.fromContext.memberId.toString())
+    assert.equal(targetMember.id, this.toContext.memberId.toString())
+    assert.equal(numberOfInvites, this.invitesToTransfer)
+  }
+
   async execute(): Promise<void> {
     const { fromContext, toContext, invitesToTransfer } = this
     const tx = this.api.tx.members.transferInvites(fromContext.memberId, toContext.memberId, invitesToTransfer)
@@ -381,19 +519,36 @@ export class TransferInvitesHappyCaseFixture extends MembershipFixture {
     ])
 
     // Send transfer invites extrinsic
-    await this.api.signAndSend(tx, fromContext.account)
+    const txRes = await this.api.signAndSend(tx, fromContext.account)
+    const event = await this.api.retrieveMembershipEventDetails(txRes, 'InvitesTransferred')
+    const txHash = tx.hash.toString()
+
+    // Check "from" member
     await this.query.tryQueryWithTimeout(
       () => this.query.getMemberById(fromContext.memberId),
-      ({ data: { membership: queriedFromMember } }) => {
-        assert.isOk(queriedFromMember)
-        assert.equal(queriedFromMember!.inviteCount, fromMember.invites.toNumber() - invitesToTransfer)
+      ({ data: { membershipByUniqueInput: queriedFromMember } }) => {
+        if (!queriedFromMember) {
+          throw new Error('Source member not found')
+        }
+        assert.equal(queriedFromMember.inviteCount, fromMember.invites.toNumber() - invitesToTransfer)
       }
     )
+
+    // Check "to" member
     const {
-      data: { membership: queriedToMember },
+      data: { membershipByUniqueInput: queriedToMember },
     } = await this.query.getMemberById(toContext.memberId)
-    assert.isOk(queriedToMember)
-    assert.equal(queriedToMember!.inviteCount, toMember.invites.toNumber() + invitesToTransfer)
+    if (!queriedToMember) {
+      throw new Error('Target member not found')
+    }
+    assert.equal(queriedToMember.inviteCount, toMember.invites.toNumber() + invitesToTransfer)
+
+    // Check event
+    const {
+      data: { invitesTransferredEvents },
+    } = await this.query.getInvitesTransferredEvents(fromContext.memberId)
+
+    this.assertQueryNodeEventIsValid(event, txHash, invitesTransferredEvents)
   }
 }
 
@@ -409,31 +564,91 @@ export class AddStakingAccountsHappyCaseFixture extends MembershipFixture {
     this.accounts = accounts
   }
 
+  private assertQueryNodeAddAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountAddedEvent[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountAddedEvent)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
+  private assertQueryNodeConfirmAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountConfirmedEvent[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountConfirmed)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
   async execute(): Promise<void> {
     const { memberContext, accounts } = this
-    const addStakingCandidateTx = this.api.tx.members.addStakingAccountCandidate(memberContext.memberId)
+    const addStakingCandidateTxs = accounts.map(() =>
+      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 addStakingCandidateFee = await this.api.estimateTxFee(addStakingCandidateTxs[0], accounts[0])
     const confirmStakingAccountFee = await this.api.estimateTxFee(confirmStakingAccountTxs[0], memberContext.account)
 
     await this.api.treasuryTransferBalance(memberContext.account, confirmStakingAccountFee.muln(accounts.length))
     const stakingAccountRequiredBalance = addStakingCandidateFee.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
     await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, stakingAccountRequiredBalance)))
     // Add staking account candidates
-    await Promise.all(accounts.map((a) => this.api.signAndSend(addStakingCandidateTx, a)))
+    const addResults = await Promise.all(accounts.map((a, i) => this.api.signAndSend(addStakingCandidateTxs[i], a)))
+    const addEvents = await Promise.all(
+      addResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountAdded'))
+    )
     // Confirm staking accounts
-    await Promise.all(confirmStakingAccountTxs.map((tx) => this.api.signAndSend(tx, memberContext.account)))
+    const confirmResults = await Promise.all(
+      confirmStakingAccountTxs.map((tx) => this.api.signAndSend(tx, memberContext.account))
+    )
+    const confirmEvents = await Promise.all(
+      confirmResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountConfirmed'))
+    )
 
     await this.query.tryQueryWithTimeout(
       () => this.query.getMemberById(memberContext.memberId),
-      ({ data: { membership } }) => {
-        assert.isOk(membership)
-        assert.isNotEmpty(membership!.boundAccounts)
-        assert.includeMembers(membership!.boundAccounts, accounts)
+      ({ data: { membershipByUniqueInput: membership } }) => {
+        if (!membership) {
+          throw new Error('Member not found')
+        }
+        assert.isNotEmpty(membership.boundAccounts)
+        assert.includeMembers(membership.boundAccounts, accounts)
       }
     )
+
+    // Check events
+    const {
+      data: { stakingAccountAddedEvents },
+    } = await this.query.getStakingAccountAddedEvents(memberContext.memberId)
+    const {
+      data: { stakingAccountConfirmedEvents },
+    } = await this.query.getStakingAccountConfirmedEvents(memberContext.memberId)
+    accounts.forEach(async (account, i) => {
+      this.assertQueryNodeAddAccountEventIsValid(
+        addEvents[i],
+        account,
+        addStakingCandidateTxs[i].hash.toString(),
+        stakingAccountAddedEvents
+      )
+      this.assertQueryNodeConfirmAccountEventIsValid(
+        confirmEvents[i],
+        account,
+        confirmStakingAccountTxs[i].hash.toString(),
+        stakingAccountConfirmedEvents
+      )
+    })
   }
 }
 
@@ -449,22 +664,56 @@ export class RemoveStakingAccountsHappyCaseFixture extends MembershipFixture {
     this.accounts = accounts
   }
 
+  private assertQueryNodeRemoveAccountEventIsValid(
+    eventDetails: EventDetails,
+    account: string,
+    txHash: string,
+    qEvents: StakingAccountRemovedEvent[]
+  ) {
+    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StakingAccountRemoved)
+    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
+    assert.equal(qEvent.account, account)
+  }
+
   async execute(): Promise<void> {
     const { memberContext, accounts } = this
-    const removeStakingAccountTx = this.api.tx.members.removeStakingAccount(memberContext.memberId)
+    const removeStakingAccountTxs = accounts.map(() => this.api.tx.members.removeStakingAccount(memberContext.memberId))
 
-    const removeStakingAccountFee = await this.api.estimateTxFee(removeStakingAccountTx, accounts[0])
+    const removeStakingAccountFee = await this.api.estimateTxFee(removeStakingAccountTxs[0], 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)))
+    const results = await Promise.all(accounts.map((a, i) => this.api.signAndSend(removeStakingAccountTxs[i], a)))
+    const events = await Promise.all(
+      results.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountRemoved'))
+    )
 
+    // Check member
     await this.query.tryQueryWithTimeout(
       () => this.query.getMemberById(memberContext.memberId),
-      ({ data: { membership } }) => {
-        assert.isOk(membership)
-        assert.notInclude(membership!.boundAccounts, accounts)
+      ({ data: { membershipByUniqueInput: membership } }) => {
+        if (!membership) {
+          throw new Error('Membership not found!')
+        }
+        accounts.forEach((a) => assert.notInclude(membership.boundAccounts, a))
       }
     )
+
+    // Check events
+    const {
+      data: { stakingAccountRemovedEvents },
+    } = await this.query.getStakingAccountRemovedEvents(memberContext.memberId)
+    await Promise.all(
+      accounts.map(async (account, i) => {
+        this.assertQueryNodeRemoveAccountEventIsValid(
+          events[i],
+          account,
+          removeStakingAccountTxs[i].hash.toString(),
+          stakingAccountRemovedEvents
+        )
+      })
+    )
   }
 }

+ 32 - 0
tests/integration-tests/src/types.ts

@@ -0,0 +1,32 @@
+import { MemberId } from '@joystream/types/common'
+import { Event } from '@polkadot/types/interfaces/system'
+
+export interface EventDetails {
+  event: Event
+  blockNumber: number
+  indexInBlock: number
+}
+
+export interface MembershipBoughtEventDetails extends EventDetails {
+  memberId: MemberId
+}
+
+export interface MemberInvitedEventDetails extends EventDetails {
+  newMemberId: MemberId
+}
+
+export type MembershipEventName =
+  | 'MembershipBought'
+  | 'MemberProfileUpdated'
+  | 'MemberAccountsUpdated'
+  | 'MemberVerificationStatusUpdated'
+  | 'InvitesTransferred'
+  | 'MemberInvited'
+  | 'StakingAccountAdded'
+  | 'StakingAccountConfirmed'
+  | 'StakingAccountRemoved'
+  | 'InitialInvitationCountUpdated'
+  | 'MembershipPriceUpdated'
+  | 'ReferralCutUpdated'
+  | 'InitialInvitationBalanceUpdated'
+  | 'LeaderInvitationQuotaUpdated'

Some files were not shown because too many files changed in this diff