Browse Source

Query node, integration tests: Support for new working groups, metadata-protobuf updates

Leszek Wiesner 3 years ago
parent
commit
4a7bf75eab

+ 0 - 13
query-node/bootstrap.sh

@@ -1,13 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
-cd $SCRIPT_PATH
-
-# Load and export variables from root .env file into shell environment
-set -a
-. ../.env
-. ./generated/graphql-server/.env
-set +a
-
-BOOTSTRAP_DATA_FOLDER=`pwd`/mappings/bootstrap/data node ./mappings/lib/mappings/bootstrap/index.js

+ 90 - 0
query-node/manifest.yml

@@ -368,6 +368,96 @@ mappings:
       handler: workingGroups_NewMissedRewardLevelReached
     - event: contentDirectoryWorkingGroup.WorkerStartedLeaving
       handler: workingGroups_WorkerStartedLeaving
+    # Operations working group
+    - event: operationsWorkingGroup.OpeningAdded
+      handler: workingGroups_OpeningAdded
+    - event: operationsWorkingGroup.AppliedOnOpening
+      handler: workingGroups_AppliedOnOpening
+    - event: operationsWorkingGroup.OpeningFilled
+      handler: workingGroups_OpeningFilled
+    - event: operationsWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet
+    - event: operationsWorkingGroup.WorkerRoleAccountUpdated
+      handler: workingGroups_WorkerRoleAccountUpdated
+    - event: operationsWorkingGroup.LeaderUnset
+      handler: workingGroups_LeaderUnset
+    - event: operationsWorkingGroup.WorkerExited
+      handler: workingGroups_WorkerExited
+    - event: operationsWorkingGroup.TerminatedWorker
+      handler: workingGroups_TerminatedWorker
+    - event: operationsWorkingGroup.TerminatedLeader
+      handler: workingGroups_TerminatedLeader
+    - event: operationsWorkingGroup.StakeSlashed
+      handler: workingGroups_StakeSlashed
+    - event: operationsWorkingGroup.StakeDecreased
+      handler: workingGroups_StakeDecreased
+    - event: operationsWorkingGroup.StakeIncreased
+      handler: workingGroups_StakeIncreased
+    - event: operationsWorkingGroup.ApplicationWithdrawn
+      handler: workingGroups_ApplicationWithdrawn
+    - event: operationsWorkingGroup.OpeningCanceled
+      handler: workingGroups_OpeningCanceled
+    - event: operationsWorkingGroup.BudgetSet
+      handler: workingGroups_BudgetSet
+    - event: operationsWorkingGroup.WorkerRewardAccountUpdated
+      handler: workingGroups_WorkerRewardAccountUpdated
+    - event: operationsWorkingGroup.WorkerRewardAmountUpdated
+      handler: workingGroups_WorkerRewardAmountUpdated
+    - event: operationsWorkingGroup.StatusTextChanged
+      handler: workingGroups_StatusTextChanged
+    - event: operationsWorkingGroup.BudgetSpending
+      handler: workingGroups_BudgetSpending
+    - event: operationsWorkingGroup.RewardPaid
+      handler: workingGroups_RewardPaid
+    - event: operationsWorkingGroup.NewMissedRewardLevelReached
+      handler: workingGroups_NewMissedRewardLevelReached
+    - event: operationsWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving
+    # Gateway working group
+    - event: gatewayWorkingGroup.OpeningAdded
+      handler: workingGroups_OpeningAdded
+    - event: gatewayWorkingGroup.AppliedOnOpening
+      handler: workingGroups_AppliedOnOpening
+    - event: gatewayWorkingGroup.OpeningFilled
+      handler: workingGroups_OpeningFilled
+    - event: gatewayWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet
+    - event: gatewayWorkingGroup.WorkerRoleAccountUpdated
+      handler: workingGroups_WorkerRoleAccountUpdated
+    - event: gatewayWorkingGroup.LeaderUnset
+      handler: workingGroups_LeaderUnset
+    - event: gatewayWorkingGroup.WorkerExited
+      handler: workingGroups_WorkerExited
+    - event: gatewayWorkingGroup.TerminatedWorker
+      handler: workingGroups_TerminatedWorker
+    - event: gatewayWorkingGroup.TerminatedLeader
+      handler: workingGroups_TerminatedLeader
+    - event: gatewayWorkingGroup.StakeSlashed
+      handler: workingGroups_StakeSlashed
+    - event: gatewayWorkingGroup.StakeDecreased
+      handler: workingGroups_StakeDecreased
+    - event: gatewayWorkingGroup.StakeIncreased
+      handler: workingGroups_StakeIncreased
+    - event: gatewayWorkingGroup.ApplicationWithdrawn
+      handler: workingGroups_ApplicationWithdrawn
+    - event: gatewayWorkingGroup.OpeningCanceled
+      handler: workingGroups_OpeningCanceled
+    - event: gatewayWorkingGroup.BudgetSet
+      handler: workingGroups_BudgetSet
+    - event: gatewayWorkingGroup.WorkerRewardAccountUpdated
+      handler: workingGroups_WorkerRewardAccountUpdated
+    - event: gatewayWorkingGroup.WorkerRewardAmountUpdated
+      handler: workingGroups_WorkerRewardAmountUpdated
+    - event: gatewayWorkingGroup.StatusTextChanged
+      handler: workingGroups_StatusTextChanged
+    - event: gatewayWorkingGroup.BudgetSpending
+      handler: workingGroups_BudgetSpending
+    - event: gatewayWorkingGroup.RewardPaid
+      handler: workingGroups_RewardPaid
+    - event: gatewayWorkingGroup.NewMissedRewardLevelReached
+      handler: workingGroups_NewMissedRewardLevelReached
+    - event: gatewayWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving
     # Proposals
     - event: proposalsCodex.ProposalCreated
       handler: proposalsCodex_ProposalCreated

+ 13 - 32
query-node/mappings/common.ts

@@ -12,6 +12,8 @@ import { Worker, Event, Network, DataObject, LiaisonJudgement, DataObjectOwner }
 import { BaseModel } from 'warthog'
 import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
 import { registry } from '@joystream/types'
+import { metaToObject } from '@joystream/metadata-protobuf/utils'
+import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
 
 export const CURRENT_NETWORK = Network.OLYMPIA
 /*
@@ -213,22 +215,12 @@ export function genericEventFields(substrateEvent: SubstrateEvent): Partial<Base
     indexInBlock,
   }
 }
-
-type AnyMessage<T> = T & {
-  toJSON(): Record<string, unknown>
-}
-
-type AnyMetadataClass<T> = {
-  name: string
-  decode(binary: Uint8Array): AnyMessage<T>
-  encode(obj: T): { finish(): Uint8Array }
-  toObject(obj: AnyMessage<T>): Record<string, unknown>
-}
-
-export function deserializeMetadata<T>(metadataType: AnyMetadataClass<T>, metadataBytes: Bytes): T | null {
+export function deserializeMetadata<T>(
+  metadataType: AnyMetadataClass<T>,
+  metadataBytes: Bytes
+): DecodedMetadataObject<T> | null {
   try {
-    // We use `toObject()` to get rid of .prototype defaults for optional fields
-    return metadataType.toObject(metadataType.decode(metadataBytes.toU8a(true))) as T
+    return metaToObject(metadataType, metadataType.decode(metadataBytes.toU8a(true)))
   } catch (e) {
     invalidMetadata(`Cannot deserialize ${metadataType.name}! Provided bytes: (${metadataBytes.toHex()})`)
     return null
@@ -249,23 +241,6 @@ export function perpareString(s: string): string {
   return s.replace(/\u0000/g, '')
 }
 
-export function isSet<T>(v: T | null | undefined): v is T {
-  return v !== null && v !== undefined
-}
-
-export function integrateMeta<
-  T extends BaseModel,
-  Props extends readonly (keyof T & keyof M & string)[],
-  M extends { [K in Props[number]]?: T[K] | null }
->(object: T, meta: M, props: Props): void {
-  props.forEach((prop) => {
-    const metaPropVal = meta[prop] as T[Props[number]] | null | undefined
-    if (isSet(metaPropVal)) {
-      object[prop] = metaPropVal
-    }
-  })
-}
-
 export function hasValuesForProperties<
   T extends Record<string, unknown>,
   P extends keyof T & string,
@@ -284,6 +259,8 @@ export type WorkingGroupModuleName =
   | 'contentDirectoryWorkingGroup'
   | 'forumWorkingGroup'
   | 'membershipWorkingGroup'
+  | 'operationsWorkingGroup'
+  | 'gatewayWorkingGroup'
 
 export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
   if (group.isContent) {
@@ -294,6 +271,10 @@ export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModu
     return 'forumWorkingGroup'
   } else if (group.isStorage) {
     return 'storageWorkingGroup'
+  } else if (group.isOperations) {
+    return 'operationsWorkingGroup'
+  } else if (group.isGateway) {
+    return 'gatewayWorkingGroup'
   }
 
   unexpectedData('Unsupported working group encountered:', group.type)

+ 3 - 2
query-node/mappings/content/channel.ts

@@ -8,8 +8,9 @@ import { Option } from '@polkadot/types/codec'
 import { Content } from '../generated/types'
 import { convertContentActorToChannelOwner, processChannelMetadata } from './utils'
 import { Channel, ChannelCategory, DataObject } from 'query-node/dist/model'
-import { deserializeMetadata, inconsistentState, integrateMeta, logger } from '../common'
-import { ChannelCategoryMetadata, ChannelMetadata } from '@joystream/metadata-protobuf/compiled'
+import { deserializeMetadata, inconsistentState, logger } from '../common'
+import { ChannelCategoryMetadata, ChannelMetadata } from '@joystream/metadata-protobuf'
+import { integrateMeta } from '@joystream/metadata-protobuf/utils'
 
 export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise<void> {
   const { store, event } = ctx

+ 10 - 16
query-node/mappings/content/utils.ts

@@ -16,15 +16,8 @@ import {
   IMediaType,
   IChannelMetadata,
 } from '@joystream/metadata-protobuf'
-import {
-  invalidMetadata,
-  inconsistentState,
-  logger,
-  isSet,
-  integrateMeta,
-  unexpectedData,
-  createDataObject,
-} from '../common'
+import { integrateMeta, isSet } from '@joystream/metadata-protobuf/utils'
+import { invalidMetadata, inconsistentState, logger, unexpectedData, createDataObject } from '../common'
 import {
   // primary entities
   CuratorGroup,
@@ -49,11 +42,12 @@ import {
 import { ContentParameters, NewAsset, ContentActor } from '@joystream/types/augment'
 import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
 import { registry } from '@joystream/types'
+import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
 
 export async function processChannelMetadata(
   ctx: EventContext & StoreContext,
   channel: Channel,
-  meta: IChannelMetadata,
+  meta: DecodedMetadataObject<IChannelMetadata>,
   assets: NewAsset[]
 ): Promise<Channel> {
   const assetsOwner = new DataObjectOwnerChannel()
@@ -65,7 +59,7 @@ export async function processChannelMetadata(
 
   // prepare channel category if needed
   if (isSet(meta.category)) {
-    channel.category = await processChannelCategory(ctx, channel.category, meta.category.toNumber())
+    channel.category = await processChannelCategory(ctx, channel.category, parseInt(meta.category))
   }
 
   // prepare cover photo asset if needed
@@ -96,7 +90,7 @@ export async function processVideoMetadata(
   ctx: EventContext & StoreContext,
   channel: Channel,
   video: Video,
-  meta: IVideoMetadata,
+  meta: DecodedMetadataObject<IVideoMetadata>,
   assets: NewAsset[]
 ): Promise<Video> {
   const assetsOwner = new DataObjectOwnerChannel()
@@ -108,7 +102,7 @@ export async function processVideoMetadata(
 
   // prepare video category if needed
   if (meta.category) {
-    video.category = await processVideoCategory(ctx, video.category, meta.category.toNumber())
+    video.category = await processVideoCategory(ctx, video.category, parseInt(meta.category))
   }
 
   // prepare media meta information if needed
@@ -171,7 +165,7 @@ function findAssetByIndex(assets: typeof Asset[], index: number, name?: string):
 async function processVideoMediaEncoding(
   { store, event }: StoreContext & EventContext,
   existingVideoMediaEncoding: VideoMediaEncoding | undefined,
-  metadata: IMediaType
+  metadata: DecodedMetadataObject<IMediaType>
 ): Promise<VideoMediaEncoding> {
   const encoding =
     existingVideoMediaEncoding ||
@@ -191,7 +185,7 @@ async function processVideoMediaEncoding(
 async function processVideoMediaMetadata(
   ctx: StoreContext & EventContext,
   existingVideoMedia: VideoMediaMetadata | undefined,
-  metadata: IVideoMetadata,
+  metadata: DecodedMetadataObject<IVideoMetadata>,
   videoSize: number | undefined
 ): Promise<VideoMediaMetadata> {
   const { store, event } = ctx
@@ -266,7 +260,7 @@ export async function convertContentActorToChannelOwner(
 function processPublishedBeforeJoystream(
   ctx: EventContext & StoreContext,
   currentValue: Date | undefined,
-  metadata: IPublishedBeforeJoystream
+  metadata: DecodedMetadataObject<IPublishedBeforeJoystream>
 ): Date | undefined {
   if (!isSet(metadata)) {
     return currentValue

+ 2 - 1
query-node/mappings/content/video.ts

@@ -4,10 +4,11 @@ eslint-disable @typescript-eslint/naming-convention
 import { EventContext, StoreContext } from '@dzlzv/hydra-common'
 import { In } from 'typeorm'
 import { Content } from '../generated/types'
-import { deserializeMetadata, inconsistentState, integrateMeta, logger } from '../common'
+import { deserializeMetadata, inconsistentState, logger } from '../common'
 import { processVideoMetadata } from './utils'
 import { Channel, Video, VideoCategory } from 'query-node/dist/model'
 import { VideoMetadata, VideoCategoryMetadata } from '@joystream/metadata-protobuf'
+import { integrateMeta } from '@joystream/metadata-protobuf/utils'
 
 export async function content_VideoCategoryCreated({ store, event }: EventContext & StoreContext): Promise<void> {
   // read event data

+ 8 - 0
query-node/mappings/genesis-data/workingGroups.json

@@ -14,5 +14,13 @@
   {
     "name": "forumWorkingGroup",
     "budget": 0
+  },
+  {
+    "name": "operationsWorkingGroup",
+    "budget": 0
+  },
+  {
+    "name": "gatewayWorkingGroup",
+    "budget": 0
   }
 ]

+ 6 - 4
query-node/mappings/workingGroups.ts

@@ -74,6 +74,8 @@ import {
   WorkerStatusLeaving,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
+import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
+import { isSet } from '@joystream/metadata-protobuf/utils'
 
 // Reusable functions
 async function getWorkingGroup(
@@ -230,7 +232,7 @@ async function handleAddUpcomingOpeningAction(
   store: DatabaseManager,
   event: SubstrateEvent,
   statusChangedEvent: StatusTextChangedEvent,
-  action: IAddUpcomingOpening
+  action: DecodedMetadataObject<IAddUpcomingOpening>
 ): Promise<UpcomingOpeningAdded | InvalidActionMetadata> {
   const upcomingOpeningMeta = action.metadata || {}
   const group = await getWorkingGroup(store, event)
@@ -246,9 +248,9 @@ async function handleAddUpcomingOpeningAction(
     updatedAt: eventTime,
     metadata: openingMeta,
     group,
-    rewardPerBlock: rewardPerBlock?.toNumber() ? new BN(rewardPerBlock.toString()) : undefined,
+    rewardPerBlock: isSet(rewardPerBlock) && parseInt(rewardPerBlock) ? new BN(rewardPerBlock) : undefined,
     expectedStart: expectedStart ? new Date(expectedStart) : undefined,
-    stakeAmount: minApplicationStake?.toNumber() ? new BN(minApplicationStake.toString()) : undefined,
+    stakeAmount: isSet(minApplicationStake) && parseInt(minApplicationStake) ? new BN(minApplicationStake) : undefined,
     createdInEvent: statusChangedEvent,
   })
   await store.save<UpcomingWorkingGroupOpening>(upcomingOpening)
@@ -318,7 +320,7 @@ async function handleWorkingGroupMetadataAction(
   store: DatabaseManager,
   event: SubstrateEvent,
   statusChangedEvent: StatusTextChangedEvent,
-  action: IWorkingGroupMetadataAction
+  action: DecodedMetadataObject<IWorkingGroupMetadataAction>
 ): Promise<typeof WorkingGroupMetadataActionResult> {
   if (action.addUpcomingOpening) {
     return handleAddUpcomingOpeningAction(store, event, statusChangedEvent, action.addUpcomingOpening)

+ 0 - 1
query-node/schemas/storage.graphql

@@ -23,7 +23,6 @@ type DataObject @entity {
 
   "Storage provider id of the liaison"
   liaison: Worker # liaison is unset until storage provider accepts or rejects the content
-
   "Storage provider as liaison judgment"
   liaisonJudgement: LiaisonJudgement!
 

+ 8 - 0
tests/integration-tests/src/consts.ts

@@ -21,6 +21,8 @@ export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
   contentDirectoryWorkingGroup: '0x0707070707070707',
   forumWorkingGroup: '0x0808080808080808',
   membershipWorkingGroup: '0x0909090909090909',
+  operationsWorkingGroup: '0x0d0d0d0d0d0d0d0d',
+  gatewayWorkingGroup: '0x0e0e0e0e0e0e0e0e',
 }
 
 export const workingGroups: WorkingGroupModuleName[] = [
@@ -28,6 +30,8 @@ export const workingGroups: WorkingGroupModuleName[] = [
   'contentDirectoryWorkingGroup',
   'forumWorkingGroup',
   'membershipWorkingGroup',
+  'operationsWorkingGroup',
+  'gatewayWorkingGroup',
 ]
 
 export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
@@ -39,6 +43,10 @@ export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModu
     return 'forumWorkingGroup'
   } else if (group.isOfType('Storage')) {
     return 'storageWorkingGroup'
+  } else if (group.isOfType('Operations')) {
+    return 'operationsWorkingGroup'
+  } else if (group.isOfType('Gateway')) {
+    return 'gatewayWorkingGroup'
   }
 
   throw new Error(`Unsupported working group: ${group}`)

+ 11 - 7
tests/integration-tests/src/fixtures/workingGroups/CreateUpcomingOpeningsFixture.ts

@@ -18,6 +18,8 @@ import { DEFAULT_OPENING_PARAMS } from './CreateOpeningsFixture'
 import { createType } from '@joystream/types'
 import { assertQueriedOpeningMetadataIsValid } from './utils'
 import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
+import { encodeDecode, isSet } from '@joystream/metadata-protobuf/utils'
 
 export const DEFAULT_UPCOMING_OPENING_META: IUpcomingOpeningMetadata = {
   minApplicationStake: Long.fromString(DEFAULT_OPENING_PARAMS.stake.toString()),
@@ -61,7 +63,9 @@ export class CreateUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
     return this.createdUpcomingOpeningIds
   }
 
-  protected getUpcomingOpeningMeta(params: UpcomingOpeningParams): IUpcomingOpeningMetadata | null {
+  protected getUpcomingOpeningMeta(
+    params: UpcomingOpeningParams
+  ): DecodedMetadataObject<IUpcomingOpeningMetadata> | null {
     if (typeof params.meta === 'string') {
       try {
         return Utils.metadataFromBytes(UpcomingOpeningMetadata, createType('Bytes', params.meta))
@@ -72,7 +76,7 @@ export class CreateUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
         return null
       }
     }
-    return params.meta
+    return encodeDecode(UpcomingOpeningMetadata, params.meta)
   }
 
   protected getActionMetadataBytes(params: UpcomingOpeningParams): Bytes {
@@ -82,7 +86,7 @@ export class CreateUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
     }
     return Utils.metadataToBytes(WorkingGroupMetadataAction, {
       addUpcomingOpening: {
-        metadata: upcomingOpeningMeta,
+        metadata: upcomingOpeningMeta as IUpcomingOpeningMetadata,
       },
     })
   }
@@ -106,14 +110,14 @@ export class CreateUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
         assert.equal(qUpcomingOpening.group.name, this.group)
         assert.equal(
           qUpcomingOpening.rewardPerBlock,
-          expectedMeta.rewardPerBlock && expectedMeta.rewardPerBlock.toNumber()
-            ? expectedMeta.rewardPerBlock.toString()
+          isSet(expectedMeta.rewardPerBlock) && parseInt(expectedMeta.rewardPerBlock)
+            ? expectedMeta.rewardPerBlock
             : null
         )
         assert.equal(
           qUpcomingOpening.stakeAmount,
-          expectedMeta.minApplicationStake && expectedMeta.minApplicationStake.toNumber()
-            ? expectedMeta.minApplicationStake.toString()
+          isSet(expectedMeta.minApplicationStake) && parseInt(expectedMeta.minApplicationStake)
+            ? expectedMeta.minApplicationStake
             : null
         )
         Utils.assert(qEvent.result.__typename === 'UpcomingOpeningAdded')

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

@@ -99,6 +99,8 @@ export type WorkingGroupModuleName =
   | 'contentDirectoryWorkingGroup'
   | 'forumWorkingGroup'
   | 'membershipWorkingGroup'
+  | 'operationsWorkingGroup'
+  | 'gatewayWorkingGroup'
 
 // Proposals:
 

+ 6 - 13
tests/integration-tests/src/utils.ts

@@ -9,16 +9,9 @@ import { createType } from '@joystream/types'
 import { extendDebug, Debugger } from './Debugger'
 import { BLOCKTIME } from './consts'
 import { MetadataInput } from './types'
+import { encodeDecode, metaToObject } from '@joystream/metadata-protobuf/utils'
+import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
 
-export type AnyMessage<T> = T & {
-  toJSON(): Record<string, unknown>
-}
-
-export type AnyMetadataClass<T> = {
-  decode(binary: Uint8Array): AnyMessage<T>
-  encode(obj: T): { finish(): Uint8Array }
-  toObject(obj: AnyMessage<T>): Record<string, unknown>
-}
 export class Utils {
   private static LENGTH_ADDRESS = 32 + 1 // publicKey + prefix
   private static LENGTH_ERA = 2 // assuming mortals
@@ -64,15 +57,15 @@ export class Utils {
     return createType('Bytes', '0x' + Buffer.from(metaClass.encode(obj).finish()).toString('hex'))
   }
 
-  public static metadataFromBytes<T>(metaClass: AnyMetadataClass<T>, bytes: Bytes): T {
+  public static metadataFromBytes<T>(metaClass: AnyMetadataClass<T>, bytes: Bytes): DecodedMetadataObject<T> {
     // We use `toObject()` to get rid of .prototype defaults for optional fields
-    return metaClass.toObject(metaClass.decode(bytes.toU8a(true))) as T
+    return metaToObject(metaClass, metaClass.decode(bytes.toU8a(true)))
   }
 
   public static getDeserializedMetadataFormInput<T>(
     metadataClass: AnyMetadataClass<T>,
     input: MetadataInput<T>
-  ): T | null {
+  ): DecodedMetadataObject<T> | null {
     if (typeof input.value === 'string') {
       try {
         return Utils.metadataFromBytes(metadataClass, createType('Bytes', input.value))
@@ -84,7 +77,7 @@ export class Utils {
       }
     }
 
-    return input.value
+    return encodeDecode(metadataClass, input.value)
   }
 
   public static getMetadataBytesFromInput<T>(metadataClass: AnyMetadataClass<T>, input: MetadataInput<T>): Bytes {