diff --git a/packages/types/src/create/lazy.ts b/packages/types/src/create/lazy.ts index 8f9494f289..ca9b94f441 100644 --- a/packages/types/src/create/lazy.ts +++ b/packages/types/src/create/lazy.ts @@ -1,19 +1,45 @@ // Copyright 2017-2021 @polkadot/types authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { isUndefined } from '@polkadot/util'; + type WithToString = { toString: () => string }; export function lazyMethod (result: Record, item: K, creator: (d: K) => T, getName?: (d: K) => string): void { - let cached: T | null = null; + const name = getName + ? getName(item) + : (item as WithToString).toString(); + let value: T | undefined; - Object.defineProperty(result, getName ? getName(item) : (item as WithToString).toString(), { + Object.defineProperty(result, name, { + // This allows for re-configuration with the embedded defineProperty below + // and ensures that on tested browsers and Node, it _will_ be redefined + // and thus short-circuited for future access + configurable: true, enumerable: true, - get: (): T => { - if (!cached) { - cached = creator(item); + + // Use a function here, we don't want to capture the outer + // this (i.e. don't use arrow functions in this context) + get: function (): T { + // This check should _always_ be false and unneeded, since we override + // with a value below ... however we ensure we are quire vigilant against + // all environment failures, so we are rather be safe than sorry + if (isUndefined(value)) { + value = creator(item); + + try { + // re-define the property as a value, next time around this + // getter will only return the computed value + Object.defineProperty(this, name, { value }); + } catch { + // ignore any errors, since this _should_ not happen due to + // the "configurable" property above. But if it ever does + // from here-on we will be the cached value the next time + // around (with a very slight dip in performance) + } } - return cached; + return value; } }); } diff --git a/packages/types/src/create/registry.ts b/packages/types/src/create/registry.ts index 4ec489a612..f56449c84b 100644 --- a/packages/types/src/create/registry.ts +++ b/packages/types/src/create/registry.ts @@ -2,12 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import type { ExtDef } from '../extrinsic/signedExtensions/types'; -import type { ChainProperties, CodecHash, DispatchErrorModule, Hash, MetadataLatest, PortableRegistry, SiLookupTypeId, SiVariant } from '../interfaces/types'; -import type { Text } from '../primitive/Text'; +import type { ChainProperties, CodecHash, DispatchErrorModule, Hash, MetadataLatest, PalletMetadataLatest, PortableRegistry, SiField, SiLookupTypeId, SiVariant } from '../interfaces/types'; import type { CallFunction, Codec, CodecHasher, Constructor, DetectCodec, DetectConstructor, RegisteredTypes, Registry, RegistryError, RegistryTypes } from '../types'; import type { CreateOptions } from './types'; -import { assert, assertReturn, BN_ZERO, formatBalance, isFunction, isString, isU8a, logger, stringCamelCase, stringify, u8aToHex } from '@polkadot/util'; +import { assert, assertReturn, BN_ZERO, formatBalance, isFunction, isString, isU8a, logger, stringCamelCase, stringify } from '@polkadot/util'; import { blake2AsU8a } from '@polkadot/util-crypto'; import { DoNotConstruct } from '../codec/DoNotConstruct'; @@ -17,14 +16,20 @@ import { expandExtensionTypes, fallbackExtensions, findUnknownExtensions } from import { GenericEventData } from '../generic/Event'; import * as baseTypes from '../index.types'; import * as definitions from '../interfaces/definitions'; -import { decorateConstants, decorateExtrinsics, filterEventsSome } from '../metadata/decorate'; +import { decorateConstants, filterCallsSome, filterEventsSome } from '../metadata/decorate'; +import { createCallFunction } from '../metadata/decorate/extrinsics'; import { Metadata } from '../metadata/Metadata'; import { createClass } from './createClass'; import { createTypeUnsafe } from './createType'; +import { lazyMethod, lazyMethods } from './lazy'; const l = logger('registry'); -function getVariantArgs (lookup: PortableRegistry, { fields }: SiVariant): string[] { +function valueToString (v: { toString: () => string }): string { + return v.toString(); +} + +function getFieldArgs (lookup: PortableRegistry, fields: SiField[]): string[] { const args = new Array(fields.length); for (let i = 0; i < fields.length; i++) { @@ -34,86 +39,111 @@ function getVariantArgs (lookup: PortableRegistry, { fields }: SiVariant): strin return args; } -function valueToString (v: Text): string { - return v.toString(); +function clearRecord (record: Record): void { + const keys = Object.keys(record); + + for (let i = 0; i < keys.length; i++) { + delete record[keys[i]]; + } +} + +function getVariants (lookup: PortableRegistry, { type }: { type: SiLookupTypeId }): SiVariant[] { + return lookup.getSiType(type).def.asVariant.variants; +} + +function getVariantStringIdx ({ index }: SiVariant): string { + return index.toString(); } // create error mapping from metadata -function injectErrors (_: Registry, metadata: Metadata, metadataErrors: Record): void { - const { lookup, pallets } = metadata.asLatest; +function injectErrors (_: Registry, { lookup, pallets }: MetadataLatest, version: number, result: Record>): void { + const lazySection = ({ errors, name }: PalletMetadataLatest, index: number): void => { + const section = stringCamelCase(name); + const creator = () => lazyMethods( + getVariants(lookup, errors.unwrap()), + ({ docs, fields, index, name }: SiVariant): RegistryError => ({ + args: getFieldArgs(lookup, fields), + docs: docs.map(valueToString), + fields, + index: index.toNumber(), + method: name.toString(), + name: name.toString(), + section + }), + getVariantStringIdx + ); + + lazyMethod(result, index, creator); + }; + + clearRecord(result); for (let i = 0; i < pallets.length; i++) { - const { errors, index, name } = pallets[i]; + const pallet = pallets[i]; - if (errors.isSome) { - const sectionIndex = metadata.version >= 12 - ? index.toNumber() - : i; - const sectionName = stringCamelCase(name); - const { variants } = lookup.getSiType(errors.unwrap().type).def.asVariant; - - for (let i = 0; i < variants.length; i++) { - const { docs, fields, index, name } = variants[i]; - const variantIndex = index.toNumber(); - - metadataErrors[u8aToHex(new Uint8Array([sectionIndex, variantIndex]))] = { - args: getVariantArgs(lookup, variants[i]), - docs: docs.map(valueToString), - fields, - index: variantIndex, - method: name.toString(), - name: name.toString(), - section: sectionName - }; - } + if (pallet.errors.isSome) { + lazySection(pallet, version >= 12 ? pallet.index.toNumber() : i); } } } // create event classes from metadata -function injectEvents (registry: Registry, metadata: Metadata, metadataEvents: Record>): void { - const { lookup, pallets } = metadata.asLatest; +function injectEvents (registry: Registry, { lookup, pallets }: MetadataLatest, version: number, result: Record>>): void { + const lazySection = ({ events, name }: PalletMetadataLatest, index: number): void => { + const section = stringCamelCase(name); + const creator = () => lazyMethods( + getVariants(lookup, events.unwrap()), + (variant: SiVariant): Constructor => { + const meta = registry.createType('EventMetadataLatest', { + ...variant, + args: getFieldArgs(lookup, variant.fields) + }); + + return class extends GenericEventData { + constructor (registry: Registry, value: Uint8Array) { + super(registry, value, meta, section, variant.name.toString()); + } + }; + }, + getVariantStringIdx + ); + + lazyMethod(result, index, creator); + }; + + clearRecord(result); + const filtered = pallets.filter(filterEventsSome); for (let i = 0; i < filtered.length; i++) { - const { events, index, name } = filtered[i]; + const pallet = filtered[i]; - const sectionIndex = metadata.version >= 12 - ? index.toNumber() - : i; - const sectionName = stringCamelCase(name); - const { variants } = lookup.getSiType(events.unwrap().type).def.asVariant; - - for (let i = 0; i < variants.length; i++) { - const { index, name } = variants[i]; - const meta = registry.createType('EventMetadataLatest', { - ...variants[i], - args: getVariantArgs(lookup, variants[i]) - }); - - metadataEvents[u8aToHex(new Uint8Array([sectionIndex, index.toNumber()]))] = class extends GenericEventData { - constructor (registry: Registry, value: Uint8Array) { - super(registry, value, meta, sectionName, name.toString()); - } - }; - } + lazySection(pallet, version >= 12 ? pallet.index.toNumber() : i); } } // create extrinsic mapping from metadata -function injectExtrinsics (registry: Registry, metadata: Metadata, metadataCalls: Record): void { - const extrinsics = decorateExtrinsics(registry, metadata.asLatest, metadata.version); - const sections = Object.values(extrinsics); +function injectExtrinsics (registry: Registry, { lookup, pallets }: MetadataLatest, version: number, result: Record>): void { + const lazySection = ({ calls, name }: PalletMetadataLatest, index: number): void => { + const section = stringCamelCase(name); + const creator = () => lazyMethods( + getVariants(lookup, calls.unwrap()), + (variant: SiVariant) => + createCallFunction(registry, lookup, variant, index, section), + getVariantStringIdx + ); - // decorate the extrinsics - for (let i = 0; i < sections.length; i++) { - const methods = Object.values(sections[i]); + lazyMethod(result, index, creator); + }; - for (let j = 0; j < methods.length; j++) { - const method = methods[j]; + clearRecord(result); - metadataCalls[u8aToHex(method.callIndex)] = method; - } + const filtered = pallets.filter(filterCallsSome); + + for (let i = 0; i < filtered.length; i++) { + const pallet = filtered[i]; + + lazySection(pallet, version >= 12 ? pallet.index.toNumber() : i); } } @@ -141,11 +171,13 @@ export class TypeRegistry implements Registry { #metadata?: MetadataLatest; - readonly #metadataCalls: Record = {}; + #metadataVersion = 0; - readonly #metadataErrors: Record = {}; + readonly #metadataCalls: Record> = {}; - readonly #metadataEvents: Record> = {}; + readonly #metadataErrors: Record> = {}; + + readonly #metadataEvents: Record>> = {}; #unknownTypes = new Map(); @@ -284,29 +316,33 @@ export class TypeRegistry implements Registry { // find a specific call public findMetaCall (callIndex: Uint8Array): CallFunction { - const hexIndex = u8aToHex(callIndex); + const [section, method] = [callIndex[0], callIndex[1]]; - return assertReturn(this.#metadataCalls[hexIndex], `findMetaCall: Unable to find Call with index ${hexIndex}/[${callIndex.toString()}]`); + return assertReturn( + this.#metadataCalls[`${section}`] && this.#metadataCalls[`${section}`][`${method}`], + () => `findMetaCall: Unable to find Call with index [${section}, ${method}]/[${callIndex.toString()}]` + ); } // finds an error public findMetaError (errorIndex: Uint8Array | DispatchErrorModule): RegistryError { - const hexIndex = u8aToHex( - isU8a(errorIndex) - ? errorIndex - : new Uint8Array([ - errorIndex.index.toNumber(), - errorIndex.error.toNumber() - ]) - ); + const [section, method] = isU8a(errorIndex) + ? [errorIndex[0], errorIndex[1]] + : [errorIndex.index.toNumber(), errorIndex.error.toNumber()]; - return assertReturn(this.#metadataErrors[hexIndex], `findMetaError: Unable to find Error with index ${hexIndex}/[${errorIndex.toString()}]`); + return assertReturn( + this.#metadataErrors[`${section}`] && this.#metadataErrors[`${section}`][`${method}`], + () => `findMetaError: Unable to find Error with index [${section}, ${method}]/[${errorIndex.toString()}]` + ); } public findMetaEvent (eventIndex: Uint8Array): Constructor { - const hexIndex = u8aToHex(eventIndex); + const [section, method] = [eventIndex[0], eventIndex[1]]; - return assertReturn(this.#metadataEvents[hexIndex], `findMetaEvent: Unable to find Event with index ${hexIndex}/[${eventIndex.toString()}]`); + return assertReturn( + this.#metadataEvents[`${section}`] && this.#metadataEvents[`${section}`][`${method}`], + () => `findMetaEvent: Unable to find Event with index [${section}, ${method}]/[${eventIndex.toString()}]` + ); } public get (name: K, withUnknown?: boolean): DetectConstructor | undefined { @@ -479,10 +515,11 @@ export class TypeRegistry implements Registry { // sets the metadata public setMetadata (metadata: Metadata, signedExtensions?: string[], userExtensions?: ExtDef): void { this.#metadata = metadata.asLatest; + this.#metadataVersion = metadata.version; - injectExtrinsics(this, metadata, this.#metadataCalls); - injectErrors(this, metadata, this.#metadataErrors); - injectEvents(this, metadata, this.#metadataEvents); + injectExtrinsics(this, this.#metadata, this.#metadataVersion, this.#metadataCalls); + injectErrors(this, this.#metadata, this.#metadataVersion, this.#metadataErrors); + injectEvents(this, this.#metadata, this.#metadataVersion, this.#metadataEvents); // setup the available extensions this.setSignedExtensions( diff --git a/packages/types/src/metadata/decorate/errors/index.spec.ts b/packages/types/src/metadata/decorate/errors/index.spec.ts index c84a13a657..fd2e8bb4d7 100644 --- a/packages/types/src/metadata/decorate/errors/index.spec.ts +++ b/packages/types/src/metadata/decorate/errors/index.spec.ts @@ -17,6 +17,7 @@ const errors = decorateErrors(registry, metadata.asLatest, metadata.version); describe('decorateErrors', (): void => { it('should return known errors', (): void => { expect(errors.balances.InsufficientBalance).toBeDefined(); + expect(errors.system.FailedToExtractRuntimeVersion).toBeDefined(); }); it('has the correct metadata for known errors', (): void => { diff --git a/packages/types/src/metadata/decorate/events/index.spec.ts b/packages/types/src/metadata/decorate/events/index.spec.ts index e69dd968b5..ff2052e673 100644 --- a/packages/types/src/metadata/decorate/events/index.spec.ts +++ b/packages/types/src/metadata/decorate/events/index.spec.ts @@ -19,6 +19,7 @@ const events = decorateEvents(registry, metadata.asLatest, metadata.version); describe('decorateEvents', (): void => { it('should return known errors', (): void => { expect(events.balances.Transfer).toBeDefined(); + expect(events.system.CodeUpdated).toBeDefined(); }); it('has the correct metadata for known errors', (): void => { diff --git a/packages/types/src/metadata/decorate/extrinsics/index.ts b/packages/types/src/metadata/decorate/extrinsics/index.ts index 57e2098318..11aa221cc7 100644 --- a/packages/types/src/metadata/decorate/extrinsics/index.ts +++ b/packages/types/src/metadata/decorate/extrinsics/index.ts @@ -16,7 +16,7 @@ export function filterCallsSome ({ calls }: PalletMetadataLatest): boolean { return calls.isSome; } -function createCallFunction (registry: Registry, lookup: PortableRegistry, variant: SiVariant, sectionIndex: number, sectionName: string): CallFunction { +export function createCallFunction (registry: Registry, lookup: PortableRegistry, variant: SiVariant, sectionIndex: number, sectionName: string): CallFunction { const { fields, index } = variant; const args = new Array>(fields.length);