Browse Source

Merge pull request #2441 from ondratra/query_node_sudo_calls_members

Query node sudo calls members
Mokhtar Naamani 3 years ago
parent
commit
b4b674d7b5

+ 70 - 0
query-node/generated/types/members.ts

@@ -613,4 +613,74 @@ export namespace Members {
       ]);
     }
   }
+  /**
+   *  Update member's all or some of handle, avatar and about text.
+   */
+  export class UpdateMembershipCall {
+    public readonly extrinsic: SubstrateExtrinsic;
+    public readonly expectedArgTypes = [
+      "MemberId",
+      "Option<Bytes>",
+      "Option<Bytes>",
+      "Option<Bytes>",
+    ];
+
+    constructor(public readonly ctx: SubstrateEvent) {
+      if (ctx.extrinsic === undefined) {
+        throw new Error(`No call data has been provided`);
+      }
+      this.extrinsic = ctx.extrinsic;
+    }
+
+    get args(): UpdateMembership_Args {
+      return new UpdateMembership_Args(this.extrinsic);
+    }
+
+    validateArgs(): boolean {
+      if (this.expectedArgTypes.length !== this.extrinsic.args.length) {
+        return false;
+      }
+      let valid = true;
+      this.expectedArgTypes.forEach((type, i) => {
+        if (type !== this.extrinsic.args[i].type) {
+          valid = false;
+        }
+      });
+      return valid;
+    }
+  }
+
+  class UpdateMembership_Args {
+    constructor(public readonly extrinsic: SubstrateExtrinsic) {}
+
+    get memberId(): MemberId {
+      return createTypeUnsafe<MemberId & Codec>(typeRegistry, "MemberId", [
+        this.extrinsic.args[0].value,
+      ]);
+    }
+
+    get handle(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[1].value]
+      );
+    }
+
+    get avatarUri(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[2].value]
+      );
+    }
+
+    get about(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[3].value]
+      );
+    }
+  }
 }

+ 1 - 0
query-node/manifest.yml

@@ -74,6 +74,7 @@ typegen:
     - members.changeMemberHandle
     - members.setRootAccount
     - members.setControllerAccount
+    - members.updateMembership
 
     # content directory
     - content.create_curator_group

+ 45 - 3
query-node/mappings/src/common.ts

@@ -88,8 +88,12 @@ export interface ISudoCallArgs<T> extends ExtrinsicArg {
 */
 export function extractExtrinsicArgs<DataParams, EventObject extends IGenericExtrinsicObject<DataParams>>(
   rawEvent: SubstrateEvent,
-  callFactory: new (event: SubstrateEvent) => EventObject
-): DataParams {
+  callFactory: new (event: SubstrateEvent) => EventObject,
+
+  // in ideal world this parameter would not be needed, but there is no way to associate parameters
+  // used in sudo to extrinsic parameters without it
+  argsIndeces: Record<keyof DataParams, number>,
+): EventObject['args'] { // this is equal to DataParams but only this notation works properly
   // escape when extrinsic info is not available
   if (!rawEvent.extrinsic) {
     throw 'Invalid event - no extrinsic set' // this should never happen
@@ -102,6 +106,44 @@ export function extractExtrinsicArgs<DataParams, EventObject extends IGenericExt
 
   // sudo extrinsic call
 
+  const callArgs = extractSudoCallParameters<DataParams>(rawEvent)
+
+  // convert naming convention (underscore_names to camelCase)
+  const clearArgs = Object.keys(callArgs.args).reduce((acc, key) => {
+    const formattedName = key.replace(/_([a-z])/g, tmp => tmp[1].toUpperCase())
+
+    acc[formattedName] = callArgs.args[key]
+
+    return acc
+  }, {} as DataParams)
+
+  // prepare partial event object
+  const partialEvent = {
+    extrinsic: {
+      args: Object.keys(argsIndeces).reduce((acc, key) => {
+        acc[(argsIndeces)[key]] = {
+          value: clearArgs[key]
+        }
+
+        return acc
+      }, [] as unknown[]),
+    } as unknown as SubstrateExtrinsic
+  } as SubstrateEvent
+
+  // create event object and extract processed args
+  const finalArgs = (new callFactory(partialEvent)).args
+
+  return finalArgs
+}
+
+/*
+  Extracts extrinsic call parameters used inside of sudo call.
+*/
+export function extractSudoCallParameters<DataParams>(rawEvent: SubstrateEvent): ISudoCallArgs<DataParams> {
+  if (!rawEvent.extrinsic) {
+    throw 'Invalid event - no extrinsic set' // this should never happen
+  }
+
   // see Substrate's sudo frame for more info about sudo extrinsics and `call` argument index
   const argIndex = false
     || (rawEvent.extrinsic.method == 'sudoAs' && 1) // who, *call*
@@ -117,7 +159,7 @@ export function extractExtrinsicArgs<DataParams, EventObject extends IGenericExt
   // typecast call arguments
   const callArgs = rawEvent.extrinsic.args[argIndex].value as unknown as ISudoCallArgs<DataParams>
 
-  return callArgs.args
+  return callArgs
 }
 
 /////////////////// Logger /////////////////////////////////////////////////////

+ 81 - 7
query-node/mappings/src/membership.ts

@@ -10,6 +10,7 @@ import {
   inconsistentState,
   logger,
   extractExtrinsicArgs,
+  extractSudoCallParameters,
 } from './common'
 import { Members } from '../../generated/types'
 import { MembershipEntryMethod, Membership } from 'query-node'
@@ -19,7 +20,15 @@ import { EntryMethod } from '@joystream/types/augment'
 export async function members_MemberRegistered(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
   const { accountId, memberId, entryMethod } = new Members.MemberRegisteredEvent(event).data
-  const { avatarUri, about, handle } = extractExtrinsicArgs(event, Members.BuyMembershipCall)
+  const { avatarUri, about, handle } = extractExtrinsicArgs(
+    event,
+    Members.BuyMembershipCall,
+    {
+      handle: 1,
+      avatarUri: 2,
+      about: 3,
+    },
+  )
 
   // create new membership
   const member = new Membership({
@@ -48,7 +57,11 @@ export async function members_MemberRegistered(db: DatabaseManager, event: Subst
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedAboutText(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { text, memberId } = extractExtrinsicArgs(event, Members.ChangeMemberAboutTextCall)
+  const { text, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, about: 3})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberAboutTextCall, {memberId: 0, text: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -74,7 +87,11 @@ export async function members_MemberUpdatedAboutText(db: DatabaseManager, event:
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedAvatar(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { uri, memberId } = extractExtrinsicArgs(event, Members.ChangeMemberAvatarCall)
+  const { uri, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, avatarUri: 2})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberAvatarCall, {memberId: 0, uri: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -100,7 +117,11 @@ export async function members_MemberUpdatedAvatar(db: DatabaseManager, event: Su
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedHandle(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { handle, memberId } = extractExtrinsicArgs(event, Members.ChangeMemberHandleCall)
+  const { handle, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, handle: 1})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberHandleCall, {memberId: 0, handle: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -126,7 +147,7 @@ export async function members_MemberUpdatedHandle(db: DatabaseManager, event: Su
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberSetRootAccount(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { newRootAccount, memberId } = extractExtrinsicArgs(event, Members.SetRootAccountCall)
+  const { newRootAccount, memberId } = extractExtrinsicArgs(event, Members.SetRootAccountCall, {memberId: 0, newRootAccount: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -152,7 +173,11 @@ export async function members_MemberSetRootAccount(db: DatabaseManager, event: S
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberSetControllerAccount(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { newControllerAccount, memberId } = extractExtrinsicArgs(event, Members.SetControllerAccountCall)
+  const { newControllerAccount, memberId } = extractExtrinsicArgs(
+    event,
+    Members.SetControllerAccountCall,
+    {memberId: 0, newControllerAccount: 1},
+  )
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -185,7 +210,14 @@ function convertBytesToString(b: Bytes | null): string {
     return ''
   }
 
-  return Buffer.from(b.toU8a(true)).toString()
+  const result = Buffer.from(b.toU8a(true)).toString()
+
+  // prevent utf-8 null character
+  if (result.match(/^\0$/)) {
+    return ''
+  }
+
+  return result
 }
 
 function convertEntryMethod(entryMethod: EntryMethod): MembershipEntryMethod {
@@ -208,3 +240,45 @@ function convertEntryMethod(entryMethod: EntryMethod): MembershipEntryMethod {
   logger.error('Not implemented entry method', {entryMethod: entryMethod.toString()})
   throw 'Not implemented entry method'
 }
+
+/*
+  Returns true if event is emitted inside of `update_membership` extrinsic.
+*/
+function isUpdateMembershipExtrinsic(event: SubstrateEvent): boolean {
+  if (!event.extrinsic) { // this should never happen
+    return false
+  }
+
+  if (event.extrinsic.method == 'updateMembership') {
+    return true
+  }
+
+  // no sudo was used to update membership -> this is not updateMembership
+  if (event.extrinsic.section != 'sudo') {
+    return false
+  }
+
+  const sudoCallParameters = extractSudoCallParameters<unknown[]>(event)
+
+  // very trivial check if update_membership extrinsic was used
+  return sudoCallParameters.args.length == 4 // memberId, handle, avatarUri, about
+}
+
+interface IUnpackedUpdateMembershipOptions {
+  memberId: MemberId
+  handle: Bytes
+  uri: Bytes
+  text: Bytes
+}
+
+/*
+  Returns unwrapped data + unite naming of uri/avatarUri and about/text
+*/
+function unpackUpdateMembershipOptions(args: Members.UpdateMembershipCall['args']): IUnpackedUpdateMembershipOptions {
+  return {
+    memberId: args.memberId,
+    handle: args.handle.unwrapOrDefault(),
+    uri: args.avatarUri.unwrapOrDefault(),
+    text: args.about.unwrapOrDefault(),
+  }
+}