// Copyright 2017-2021 @polkadot/metadata authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { FunctionArgumentMetadataLatest, FunctionMetadataLatest } from '../interfaces/metadata'; import type { AnyJson, AnyTuple, AnyU8a, ArgsDef, CallBase, CallFunction, IMethod, Registry } from '../types'; import { isHex, isObject, isU8a, u8aToU8a } from '@polkadot/util'; import { Struct } from '../codec/Struct'; import { U8aFixed } from '../codec/U8aFixed'; import { getTypeClass } from '../create/createClass'; import { getTypeDef } from '../create/getTypeDef'; interface DecodeMethodInput { args: unknown; // eslint-disable-next-line no-use-before-define callIndex: GenericCallIndex | Uint8Array; } interface DecodedMethod extends DecodeMethodInput { argsDef: ArgsDef; meta: FunctionMetadataLatest; } /** * Get a mapping of `argument name -> argument type` for the function, from * its metadata. * * @param meta - The function metadata used to get the definition. * @internal */ function getArgsDef (registry: Registry, meta: FunctionMetadataLatest): ArgsDef { // eslint-disable-next-line @typescript-eslint/no-use-before-define return GenericCall.filterOrigin(meta).reduce((result, { name, type }): ArgsDef => { const Type = getTypeClass(registry, getTypeDef(type)); result[name.toString()] = Type; return result; }, {} as ArgsDef); } /** @internal */ function decodeCallViaObject (registry: Registry, value: DecodedMethod, _meta?: FunctionMetadataLatest): DecodedMethod { // we only pass args/methodsIndex out const { args, callIndex } = value; // Get the correct lookupIndex // eslint-disable-next-line @typescript-eslint/no-use-before-define const lookupIndex = callIndex instanceof GenericCallIndex ? callIndex.toU8a() : callIndex; // Find metadata with callIndex const meta = _meta || registry.findMetaCall(lookupIndex).meta; return { args, argsDef: getArgsDef(registry, meta), callIndex, meta }; } /** @internal */ function decodeCallViaU8a (registry: Registry, value: Uint8Array, _meta?: FunctionMetadataLatest): DecodedMethod { // We need 2 bytes for the callIndex const callIndex = new Uint8Array(2); callIndex.set(value.subarray(0, 2), 0); // Find metadata with callIndex const meta = _meta || registry.findMetaCall(callIndex).meta; return { args: value.subarray(2), argsDef: getArgsDef(registry, meta), callIndex, meta }; } /** * Decode input to pass into constructor. * * @param value - Value to decode, one of: * - hex * - Uint8Array * - {@see DecodeMethodInput} * @param _meta - Metadata to use, so that `injectMethods` lookup is not * necessary. * @internal */ function decodeCall (registry: Registry, value: unknown | DecodedMethod | Uint8Array | string = new Uint8Array(), _meta?: FunctionMetadataLatest): DecodedMethod { if (isHex(value) || isU8a(value)) { return decodeCallViaU8a(registry, u8aToU8a(value as string), _meta); } else if (isObject(value) && value.callIndex && value.args) { return decodeCallViaObject(registry, value as DecodedMethod, _meta); } throw new Error(`Call: Cannot decode value '${value as string}' of type ${typeof value}`); } /** * @name GenericCallIndex * @description * A wrapper around the `[sectionIndex, methodIndex]` value that uniquely identifies a method */ export class GenericCallIndex extends U8aFixed { constructor (registry: Registry, value?: AnyU8a) { super(registry, value, 16); } } /** * @name GenericCall * @description * Extrinsic function descriptor */ export class GenericCall extends Struct implements CallBase { protected _meta: FunctionMetadataLatest; constructor (registry: Registry, value: unknown, meta?: FunctionMetadataLatest) { const decoded = decodeCall(registry, value, meta); try { super(registry, { callIndex: GenericCallIndex, // eslint-disable-next-line sort-keys args: Struct.with(decoded.argsDef) }, decoded); } catch (error) { let method = 'unknown.unknown'; try { const c = registry.findMetaCall(decoded.callIndex); method = `${c.section}.${c.method}`; } catch (error) { // ignore } throw new Error(`Call: failed decoding ${method}:: ${(error as Error).message}`); } this._meta = decoded.meta; } // If the extrinsic function has an argument of type `Origin`, we ignore it public static filterOrigin (meta?: FunctionMetadataLatest): FunctionArgumentMetadataLatest[] { // FIXME should be `arg.type !== Origin`, but doesn't work... return meta ? meta.args.filter(({ type }): boolean => type.toString() !== 'Origin' ) : []; } /** * @description The arguments for the function call */ public get args (): A { // FIXME This should return a Struct instead of an Array return [...(this.get('args') as Struct).values()] as A; } /** * @description The argument definitions */ public get argsDef (): ArgsDef { return getArgsDef(this.registry, this.meta); } /** * @description The encoded `[sectionIndex, methodIndex]` identifier */ public get callIndex (): Uint8Array { return (this.get('callIndex') as GenericCallIndex).toU8a(); } /** * @description The encoded data */ public get data (): Uint8Array { return (this.get('args') as Struct).toU8a(); } /** * @description The [[FunctionMetadata]] */ public get meta (): FunctionMetadataLatest { return this._meta; } /** * @description Returns the name of the method */ public get method (): string { return this.registry.findMetaCall(this.callIndex).method; } /** * @description Returns the module containing the method */ public get section (): string { return this.registry.findMetaCall(this.callIndex).section; } /** * @description Checks if the source matches this in type */ public is (other: IMethod): other is IMethod { return other.callIndex[0] === this.callIndex[0] && other.callIndex[1] === this.callIndex[1]; } /** * @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information */ public toHuman (isExpanded?: boolean): Record { let call: CallFunction | undefined; try { call = this.registry.findMetaCall(this.callIndex); } catch (error) { // swallow } return { args: this.args.map((arg) => arg.toHuman(isExpanded)), // args: this.args.map((arg, index) => call // ? { [call.meta.args[index].name.toString()]: arg.toHuman(isExpanded) } // : arg.toHuman(isExpanded) // ), // callIndex: u8aToHex(this.callIndex), method: call?.method, section: call?.section, ...(isExpanded && call ? { documentation: call.meta.documentation.map((d) => d.toString()) } : {} ) }; } /** * @description Returns the base runtime type name for this instance */ public toRawType (): string { return 'Call'; } }