From 6e620e3bb3238c8efe83b0d84412676dc4252e94 Mon Sep 17 00:00:00 2001 From: Jaco Date: Fri, 23 Jul 2021 19:04:14 +0300 Subject: [PATCH] Short-circuit full type (with lookupIndex) (#3804) * Short-circuit full type (with lookupIndex) * Defaults for type expansion * Tuple with extra spaces removed * Fill in generated types * Adjust generation * Adjust tests * Adjust --- .../test/compare/ink_erc20.test.json | 8 +- .../test/compare/ink_erc721.test.json | 8 +- .../test/compare/ink_multisigPlain.test.json | 10 +- .../test/compare/user_withString.test.json | 8 +- packages/types/src/create/encodeTypes.spec.ts | 11 +- packages/types/src/create/encodeTypes.ts | 106 +++++++++++------- packages/types/src/create/types.ts | 4 +- .../types/src/generic/PortableRegistry.ts | 69 ++++++++++-- 8 files changed, 152 insertions(+), 72 deletions(-) diff --git a/packages/api-contract/test/compare/ink_erc20.test.json b/packages/api-contract/test/compare/ink_erc20.test.json index 9e41218522..7c2e96c702 100644 --- a/packages/api-contract/test/compare/ink_erc20.test.json +++ b/packages/api-contract/test/compare/ink_erc20.test.json @@ -158,12 +158,12 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)", + "type": "(AccountId,AccountId)", "name": "Occupied" } ], "info": 3, - "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId, AccountId)\"}}" + "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId,AccountId)\"}}" }, { "info": 10, @@ -181,7 +181,7 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)" + "type": "(AccountId,AccountId)" }, { "info": 6, @@ -198,4 +198,4 @@ "info": 5, "type": "Option" } -] \ No newline at end of file +] diff --git a/packages/api-contract/test/compare/ink_erc721.test.json b/packages/api-contract/test/compare/ink_erc721.test.json index 179e7d7f35..4166b70353 100644 --- a/packages/api-contract/test/compare/ink_erc721.test.json +++ b/packages/api-contract/test/compare/ink_erc721.test.json @@ -206,12 +206,12 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)", + "type": "(AccountId,AccountId)", "name": "Occupied" } ], "info": 3, - "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId, AccountId)\"}}" + "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId,AccountId)\"}}" }, { "info": 10, @@ -229,7 +229,7 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)" + "type": "(AccountId,AccountId)" }, { "displayName": "ValueEntry", @@ -427,4 +427,4 @@ ], "type": "{\"_enum\":[\"NotOwner\",\"NotApproved\",\"TokenExists\",\"TokenNotFound\",\"CannotInsert\",\"CannotRemove\",\"CannotFetchValue\",\"NotAllowed\"]}" } -] \ No newline at end of file +] diff --git a/packages/api-contract/test/compare/ink_multisigPlain.test.json b/packages/api-contract/test/compare/ink_multisigPlain.test.json index 846fc352fe..793e811344 100644 --- a/packages/api-contract/test/compare/ink_multisigPlain.test.json +++ b/packages/api-contract/test/compare/ink_multisigPlain.test.json @@ -63,12 +63,12 @@ "type": "AccountId" } ], - "type": "(u32, AccountId)", + "type": "(u32,AccountId)", "name": "Occupied" } ], "info": 3, - "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(u32, AccountId)\"}}" + "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(u32,AccountId)\"}}" }, { "info": 10, @@ -84,7 +84,7 @@ "type": "AccountId" } ], - "type": "(u32, AccountId)" + "type": "(u32,AccountId)" }, { "displayName": "AccountId", @@ -409,7 +409,7 @@ "type": "{\"_enum\":{\"Confirmed\":\"()\",\"ConfirmationsNeeded\":\"u32\"}}" } ], - "type": "(u32, ConfirmationStatus)" + "type": "(u32,ConfirmationStatus)" }, { "displayName": "ConfirmationStatus", @@ -511,4 +511,4 @@ "info": 5, "type": "Option>" } -] \ No newline at end of file +] diff --git a/packages/api-contract/test/compare/user_withString.test.json b/packages/api-contract/test/compare/user_withString.test.json index a57331236d..35b8c635f9 100644 --- a/packages/api-contract/test/compare/user_withString.test.json +++ b/packages/api-contract/test/compare/user_withString.test.json @@ -158,12 +158,12 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)", + "type": "(AccountId,AccountId)", "name": "Occupied" } ], "info": 3, - "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId, AccountId)\"}}" + "type": "{\"_enum\":{\"Vacant\":\"VacantEntry\",\"Occupied\":\"(AccountId,AccountId)\"}}" }, { "info": 10, @@ -181,7 +181,7 @@ "type": "AccountId" } ], - "type": "(AccountId, AccountId)" + "type": "(AccountId,AccountId)" }, { "info": 6, @@ -202,4 +202,4 @@ "info": 5, "type": "Option" } -] \ No newline at end of file +] diff --git a/packages/types/src/create/encodeTypes.spec.ts b/packages/types/src/create/encodeTypes.spec.ts index 1e547a01d8..3c0e65b95d 100644 --- a/packages/types/src/create/encodeTypes.spec.ts +++ b/packages/types/src/create/encodeTypes.spec.ts @@ -2,12 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import { encodeTypeDef } from './encodeTypes'; +import { TypeRegistry } from './registry'; import { TypeDefInfo } from './types'; describe('encodeTypeDef', (): void => { + const registry = new TypeRegistry(); + it('correctly encodes a complex struct', (): void => { expect( - JSON.parse(encodeTypeDef({ + JSON.parse(encodeTypeDef(registry, { info: TypeDefInfo.Struct, sub: [ { @@ -47,7 +50,7 @@ describe('encodeTypeDef', (): void => { it('correctly encodes a complex struct (named)', (): void => { expect( - JSON.parse(encodeTypeDef({ + JSON.parse(encodeTypeDef(registry, { info: TypeDefInfo.Struct, sub: [ { @@ -88,7 +91,7 @@ describe('encodeTypeDef', (): void => { it('correctly encodes a complex enum', (): void => { expect( - JSON.parse(encodeTypeDef({ + JSON.parse(encodeTypeDef(registry, { info: TypeDefInfo.Enum, sub: [ { @@ -148,7 +151,7 @@ describe('encodeTypeDef', (): void => { it('correctly encodes a complex enum (named)', (): void => { expect( - JSON.parse(encodeTypeDef({ + JSON.parse(encodeTypeDef(registry, { info: TypeDefInfo.Enum, sub: [ { diff --git a/packages/types/src/create/encodeTypes.ts b/packages/types/src/create/encodeTypes.ts index 8b5fde11ac..ad9ae80675 100644 --- a/packages/types/src/create/encodeTypes.ts +++ b/packages/types/src/create/encodeTypes.ts @@ -20,7 +20,7 @@ export function paramsNotation (outer: string, inner?: T | T[], transform: ( }`; } -function encodeWithParams (typeDef: TypeDef, outer: string): string { +function encodeWithParams (registry: Registry, typeDef: TypeDef, lookupCounter: number, outer: string): string { const { info, sub } = typeDef; switch (info) { @@ -32,24 +32,25 @@ function encodeWithParams (typeDef: TypeDef, outer: string): string { case TypeDefInfo.Option: case TypeDefInfo.Result: case TypeDefInfo.Vec: - return paramsNotation(outer, sub, (param) => encodeTypeDef(param)); + return paramsNotation(outer, sub, (param) => encodeTypeDef(registry, param, lookupCounter)); } throw new Error(`Unable to encode ${stringify(typeDef)} with params`); } -function encodeDoNotConstruct ({ displayName }: TypeDef): string { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function encodeDoNotConstruct (registry: Registry, { displayName }: TypeDef, lookupCounter: number): string { return `DoNotConstruct<${displayName || 'Unknown'}>`; } -function encodeSubTypes (sub: TypeDef[], asEnum?: boolean): string { +function encodeSubTypes (registry: Registry, sub: TypeDef[], lookupCounter: number, asEnum?: boolean): string { const names = sub.map(({ name }) => name); assert(names.every((n) => !!n), () => `Subtypes does not have consistent names, ${names.join(', ')}`); const inner = sub.reduce< Record>((result, type) => ({ ...result, - [type.name as string]: encodeTypeDef(type) + [type.name as string]: encodeTypeDef(registry, type, lookupCounter) }), {}); return stringify( @@ -59,7 +60,7 @@ function encodeSubTypes (sub: TypeDef[], asEnum?: boolean): string { ); } -function encodeEnum (typeDef: TypeDef): string { +function encodeEnum (registry: Registry, typeDef: TypeDef, lookupCounter: number): string { assert(typeDef.sub && Array.isArray(typeDef.sub), 'Unable to encode Enum type'); const sub = typeDef.sub; @@ -68,28 +69,29 @@ function encodeEnum (typeDef: TypeDef): string { // TODO We need to take the disciminant into account and auto-add empty entries return sub.every(({ type }) => type === 'Null') ? stringify({ _enum: sub.map(({ name }, index) => `${name || `Empty${index}`}`) }) - : encodeSubTypes(sub, true); + : encodeSubTypes(registry, sub, lookupCounter, true); } -function encodeStruct (typeDef: TypeDef): string { +function encodeStruct (registry: Registry, typeDef: TypeDef, lookupCounter: number): string { assert(typeDef.sub && Array.isArray(typeDef.sub), 'Unable to encode Struct type'); - return encodeSubTypes(typeDef.sub); + return encodeSubTypes(registry, typeDef.sub, lookupCounter); } -function encodeTuple (typeDef: TypeDef): string { +function encodeTuple (registry: Registry, typeDef: TypeDef, lookupCounter: number): string { assert(typeDef.sub && Array.isArray(typeDef.sub), 'Unable to encode Tuple type'); - return `(${typeDef.sub.map((type) => encodeTypeDef(type)).join(', ')})`; + return `(${typeDef.sub.map((type) => encodeTypeDef(registry, type, lookupCounter)).join(',')})`; } -function encodeUInt ({ length }: TypeDef, type: 'Int' | 'UInt'): string { +function encodeUInt (registry: Registry, { length }: TypeDef, lookupCounter: number, type: 'Int' | 'UInt'): string { assert(isNumber(length), 'Unable to encode VecFixed type'); return `${type}<${length}>`; } -function encodeVecFixed ({ length, sub }: TypeDef): string { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function encodeVecFixed (registry: Registry, { length, sub }: TypeDef, lookupCounter: number): string { assert(isNumber(length) && !isUndefined(sub) && !Array.isArray(sub), 'Unable to encode VecFixed type'); return `[${sub.type};${length}]`; @@ -97,38 +99,62 @@ function encodeVecFixed ({ length, sub }: TypeDef): string { // We setup a record here to ensure we have comprehensive coverage (any item not covered will result // in a compile-time error with the missing index) -const encoders: Record string> = { - [TypeDefInfo.BTreeMap]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'BTreeMap'), - [TypeDefInfo.BTreeSet]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'BTreeSet'), - [TypeDefInfo.Compact]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'Compact'), - [TypeDefInfo.DoNotConstruct]: (typeDef: TypeDef): string => encodeDoNotConstruct(typeDef), - [TypeDefInfo.Enum]: (typeDef: TypeDef): string => encodeEnum(typeDef), - [TypeDefInfo.HashMap]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'HashMap'), - [TypeDefInfo.Int]: (typeDef: TypeDef): string => encodeUInt(typeDef, 'Int'), - [TypeDefInfo.Linkage]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'Linkage'), +const encoders: Record string> = { + [TypeDefInfo.BTreeMap]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'BTreeMap'), + [TypeDefInfo.BTreeSet]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'BTreeSet'), + [TypeDefInfo.Compact]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'Compact'), + [TypeDefInfo.DoNotConstruct]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeDoNotConstruct(registry, typeDef, lookupCounter), + [TypeDefInfo.Enum]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeEnum(registry, typeDef, lookupCounter), + [TypeDefInfo.HashMap]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'HashMap'), + [TypeDefInfo.Int]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeUInt(registry, typeDef, lookupCounter, 'Int'), + [TypeDefInfo.Linkage]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'Linkage'), // eslint-disable-next-line @typescript-eslint/no-unused-vars - [TypeDefInfo.Null]: (typeDef: TypeDef): string => 'Null', - [TypeDefInfo.Option]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'Option'), - [TypeDefInfo.Plain]: (typeDef: TypeDef): string => typeDef.displayName || typeDef.type, - [TypeDefInfo.Result]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'Result'), - [TypeDefInfo.Set]: (typeDef: TypeDef): string => typeDef.type, - [TypeDefInfo.Si]: (typeDef: TypeDef): string => typeDef.type, - [TypeDefInfo.Struct]: (typeDef: TypeDef): string => encodeStruct(typeDef), - [TypeDefInfo.Tuple]: (typeDef: TypeDef): string => encodeTuple(typeDef), - [TypeDefInfo.UInt]: (typeDef: TypeDef): string => encodeUInt(typeDef, 'UInt'), - [TypeDefInfo.Vec]: (typeDef: TypeDef): string => encodeWithParams(typeDef, 'Vec'), - [TypeDefInfo.VecFixed]: (typeDef: TypeDef): string => encodeVecFixed(typeDef) + [TypeDefInfo.Null]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + 'Null', + [TypeDefInfo.Option]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'Option'), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [TypeDefInfo.Plain]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + typeDef.displayName || typeDef.type, + [TypeDefInfo.Result]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'Result'), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [TypeDefInfo.Set]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + typeDef.type, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [TypeDefInfo.Si]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + typeDef.lookupName || typeDef.type, + [TypeDefInfo.Struct]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeStruct(registry, typeDef, lookupCounter), + [TypeDefInfo.Tuple]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeTuple(registry, typeDef, lookupCounter), + [TypeDefInfo.UInt]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeUInt(registry, typeDef, lookupCounter, 'UInt'), + [TypeDefInfo.Vec]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeWithParams(registry, typeDef, lookupCounter, 'Vec'), + [TypeDefInfo.VecFixed]: (registry: Registry, typeDef: TypeDef, lookupCounter: number) => + encodeVecFixed(registry, typeDef, lookupCounter) }; -function encodeType (typeDef: TypeDef): string { +function encodeType (registry: Registry, typeDef: TypeDef, lookupCounter: number): string { const encoder = encoders[typeDef.info]; - assert(encoder, () => `Cannot encode type: info=${typeDef.info}, typeDef=${stringify(typeDef)}`); + assert(encoder, () => `Cannot encode type ${stringify(typeDef)}`); - return encoder(typeDef); + return lookupCounter < 0 && isNumber(typeDef.lookupIndex) + ? typeDef.lookupName || registry.createLookupType(typeDef.lookupIndex) + : encoder(registry, typeDef, lookupCounter--); } -export function encodeTypeDef (typeDef: TypeDef): string { +export function encodeTypeDef (registry: Registry, typeDef: TypeDef, lookupCounter = 0): string { assert(!isUndefined(typeDef.info), () => `Invalid type definition with no instance info, typeDef=${stringify(typeDef)}`); // In the case of contracts we do have the unfortunate situation where the displayName would @@ -138,12 +164,12 @@ export function encodeTypeDef (typeDef: TypeDef): string { return typeDef.displayName; } - return encodeType(typeDef); + return encodeType(registry, typeDef, lookupCounter); } -export function withTypeString (_: Registry, typeDef: Omit): TypeDef { +export function withTypeString (registry: Registry, typeDef: Omit, lookupCounter = 0): TypeDef { return { ...typeDef, - type: encodeType(typeDef as TypeDef) + type: encodeType(registry, typeDef as TypeDef, lookupCounter) }; } diff --git a/packages/types/src/create/types.ts b/packages/types/src/create/types.ts index cbd3ce539c..42a17807d7 100644 --- a/packages/types/src/create/types.ts +++ b/packages/types/src/create/types.ts @@ -36,9 +36,11 @@ export interface TypeDef { displayName?: string; fallbackType?: string; info: TypeDefInfo; - isFromSi?: boolean; index?: number; + isFromSi?: boolean; length?: number; + lookupIndex?: number; + lookupName?: string; name?: string; namespace?: string; sub?: TypeDef | TypeDef[]; diff --git a/packages/types/src/generic/PortableRegistry.ts b/packages/types/src/generic/PortableRegistry.ts index 4f842e5030..b7e835f63c 100644 --- a/packages/types/src/generic/PortableRegistry.ts +++ b/packages/types/src/generic/PortableRegistry.ts @@ -7,7 +7,7 @@ import type { SiField, SiLookupTypeId, SiType, SiTypeDefArray, SiTypeDefCompact, import type { Type } from '../primitive/Type'; import type { Registry, TypeDef } from '../types'; -import { assert, isNumber, isString, stringCamelCase, stringify } from '@polkadot/util'; +import { assert, isNumber, isString, stringCamelCase, stringify, stringUpperFirst } from '@polkadot/util'; import { Struct } from '../codec/Struct'; import { withTypeString } from '../create/encodeTypes'; @@ -30,13 +30,52 @@ const PRIMITIVE_SP = [ 'pallet_identity::types::Data' ]; +function removeDuplicateNames (names: (string | null)[]): (string | null)[] { + return names.map((name, index): string | null => + !name || names.some((o, i) => index !== i && name === o) + ? null + : name + ); +} + +function extractNames (types: PortableType[]): Record { + return removeDuplicateNames( + types.map(({ type: { path } }): string | null => { + if (!path.length) { + return null; + } + + const parts = path.map((p) => stringUpperFirst(stringCamelCase(p))); + + if (parts.length >= 2 && parts[parts.length - 1] === parts[parts.length - 2]) { + parts.pop(); + } + + const typeName = parts.join(''); + + return ['BTreeMap', 'Cow', 'Result', 'Option'].includes(typeName) || ['frame_support'].includes(path[0].toString()) + ? null + : typeName; + }) + ).reduce>((all, name, index) => { + if (name) { + all[index] = name; + } + + return all; + }, {}); +} + export class GenericPortableRegistry extends Struct { + #names: Record; #typeDefs: Record = {}; constructor (registry: Registry, value?: Uint8Array) { super(registry, { types: 'Vec' }, value); + + this.#names = extractNames(this.types); } /** @@ -61,34 +100,39 @@ export class GenericPortableRegistry extends Struct { * @description Lookup the type definition for the index */ public getTypeDef (lookupId: SiLookupTypeId | string | number): TypeDef { - const index = this.#getLookupId(lookupId); + const lookupIndex = this.#getLookupId(lookupId); - if (!this.#typeDefs[index]) { + if (!this.#typeDefs[lookupIndex]) { // we set first since we will get into circular lookups along the way - this.#typeDefs[index] = { + this.#typeDefs[lookupIndex] = { info: TypeDefInfo.DoNotConstruct, - type: this.registry.createLookupType(index) + lookupIndex, + lookupName: this.#names[lookupIndex], + type: this.registry.createLookupType(lookupIndex) }; - const typeDef = this.#extract(this.getSiType(lookupId), index); + const extracted = this.#extract(this.getSiType(lookupId), lookupIndex); - Object.keys(typeDef).forEach((key): void => { + Object.keys(extracted).forEach((key): void => { // these are safe since we are looking through the keys as set - this.#typeDefs[index][key as 'info'] = typeDef[key as 'info']; + this.#typeDefs[lookupIndex][key as 'info'] = extracted[key as 'info']; }); } - return this.#typeDefs[index]; + return this.#typeDefs[lookupIndex]; } #createSiDef (lookupId: SiLookupTypeId): TypeDef { const typeDef = this.getTypeDef(lookupId); + const lookupIndex = lookupId.toNumber(); // Flatten Plain types immediately, otherwise setup for a lookup return typeDef.info === TypeDefInfo.Plain ? typeDef : { info: TypeDefInfo.Si, + lookupIndex, + lookupName: this.#names[lookupIndex], type: this.registry.createLookupType(lookupId) }; } @@ -254,7 +298,12 @@ export class GenericPortableRegistry extends Struct { } #extractTuple (_: number, ids: SiTypeDefTuple): TypeDef { - if (ids.length === 1) { + if (ids.length === 0) { + return { + info: TypeDefInfo.Null, + type: 'Null' + }; + } else if (ids.length === 1) { return this.getTypeDef(ids[0]); }