diff --git a/packages/types/src/codec/AbstractInt.ts b/packages/types/src/codec/AbstractInt.ts index 83b256f389..506aa2deea 100644 --- a/packages/types/src/codec/AbstractInt.ts +++ b/packages/types/src/codec/AbstractInt.ts @@ -45,7 +45,7 @@ export default abstract class AbstractInt extends BN implements Codec { // This function returns a string, which will be passed in the BN // constructor. It would be ideal to actually return a BN, but there's a // bug: https://github.com/indutny/bn.js/issues/206. - if (isHex(value)) { + if (isHex(value, -1, true)) { return hexToBn(value, { isLe: false, isNegative }).toString(); } else if (isU8a(value)) { return AbstractInt.decodeAbstracIntU8a(value, bitLength, isNegative); diff --git a/packages/types/src/codec/Compact.ts b/packages/types/src/codec/Compact.ts index 3bf7bcae24..300797fb9b 100644 --- a/packages/types/src/codec/Compact.ts +++ b/packages/types/src/codec/Compact.ts @@ -5,7 +5,7 @@ import { AnyNumber, Codec, Constructor, InterfaceTypes } from '../types'; import BN from 'bn.js'; -import { bnToBn, compactAddLength, compactFromU8a, compactStripLength, compactToU8a, hexToBn, isBn, isHex, isNumber, isString } from '@polkadot/util'; +import { compactAddLength, compactFromU8a, compactStripLength, compactToU8a, isBn, isNumber, isString } from '@polkadot/util'; import { DEFAULT_BITLENGTH } from '@polkadot/util/compact/defaults'; import { typeToConstructor } from './utils'; @@ -59,14 +59,8 @@ export default class Compact extends Base { public static decodeCompact (Type: Constructor, value: Compact | AnyNumber): CompactEncodable { if (value instanceof Compact) { return new Type(value.raw); - } else if (isString(value)) { - return new Type( - isHex(value, -1, true) - ? hexToBn(value) - : new BN(value, 10) - ); - } else if (isNumber(value) || isBn(value)) { - return new Type(bnToBn(value)); + } else if (isString(value) || isNumber(value) || isBn(value)) { + return new Type(value); } const [, _value] = Compact.decodeU8a(value, new Type(0).bitLength()); diff --git a/packages/types/src/codec/EnumType.spec.ts b/packages/types/src/codec/Enum.spec.ts similarity index 100% rename from packages/types/src/codec/EnumType.spec.ts rename to packages/types/src/codec/Enum.spec.ts diff --git a/packages/types/src/codec/Enum.ts b/packages/types/src/codec/Enum.ts index d53d8ca1c7..04ac4904fd 100644 --- a/packages/types/src/codec/Enum.ts +++ b/packages/types/src/codec/Enum.ts @@ -2,6 +2,279 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import Enum from './EnumType'; +import { AnyJson, Codec, Constructor, InterfaceTypes } from '../types'; -export default Enum; +import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, stringCamelCase, stringUpperFirst, u8aConcat, u8aToHex } from '@polkadot/util'; + +import Null from '../primitive/Null'; +import { mapToTypeMap } from './utils'; +import Base from './Base'; +import Struct from './Struct'; + +interface EnumConstructor { + new(value?: any, index?: number): T; +} + +type TypesDef = Record; + +interface Decoded { + index: number; + value: Codec; +} + +function extractDef (_def: Record | string[]): { def: TypesDef; isBasic: boolean } { + if (!Array.isArray(_def)) { + const def = mapToTypeMap(_def); + const isBasic = !Object.values(def).some((type): boolean => type !== Null); + + return { + def, + isBasic + }; + } + + return { + def: _def.reduce((def, key): TypesDef => { + def[key] = Null; + + return def; + }, {} as unknown as TypesDef), + isBasic: true + }; +} + +/** + * @name Enum + * @description + * This implements an enum, that based on the value wraps a different type. It is effectively + * an extension to enum where the value type is determined by the actual index. + */ +// TODO: +// - As per Enum, actually use TS enum +// - It should rather probably extend Enum instead of copying code +export default class Enum extends Base { + private _def: TypesDef; + + private _index: number; + + private _indexes: number[]; + + private _isBasic: boolean; + + public constructor (def: Record | string[], value?: any, index?: number | Enum) { + const defInfo = extractDef(def); + const decoded = Enum.decodeEnum(defInfo.def, value, index); + + super(decoded.value); + + this._def = defInfo.def; + this._isBasic = defInfo.isBasic; + this._indexes = Object.keys(defInfo.def).map((_, index): number => index); + this._index = this._indexes.indexOf(decoded.index) || 0; + } + + private static decodeEnum (def: TypesDef, value?: any, index?: number | Enum): Decoded { + if (value instanceof Enum) { + return Enum.createValue(def, value._index, value.raw); + } else if (index instanceof Enum) { + return Enum.createValue(def, index._index, index.raw); + } else if (isNumber(index)) { + return Enum.createValue(def, index, value); + } + + // Or else, we just look at `value` + return Enum.decodeViaValue(def, value); + } + + private static decodeViaValue (def: TypesDef, value?: any): Decoded { + if (isU8a(value)) { + return Enum.createValue(def, value[0], value.subarray(1)); + } else if (isNumber(value)) { + return Enum.createValue(def, value); + } else if (isString(value)) { + return Enum.decodeViaString(def, value.toString()); + } else if (isObject(value)) { + const key = Object.keys(value)[0]; + + return Enum.createViaJSON(def, key, value[key]); + } + + // Worst-case scenario, return the first with default + return Enum.createValue(def, 0); + } + + private static decodeViaString (def: TypesDef, value: string): Decoded { + return isHex(value) + ? Enum.decodeViaValue(def, hexToU8a(value)) + : Enum.createViaJSON(def, value); + } + + private static createViaJSON (def: TypesDef, key: string, value?: any): Decoded { + // JSON comes in the form of { "": "" }, here we + // additionally force to lower to ensure forward compat + const keys = Object.keys(def).map((k): string => k.toLowerCase()); + const keyLower = key.toLowerCase(); + const index = keys.indexOf(keyLower); + + assert(index !== -1, `Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`); + + return Enum.createValue(def, index, value); + } + + private static createValue (def: TypesDef, index: number = 0, value?: any): Decoded { + const Clazz = Object.values(def)[index]; + + assert(!isUndefined(Clazz), `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`); + + return { + index, + value: new Clazz(value) + }; + } + + public static with (Types: Record | string[]): EnumConstructor { + return class extends Enum { + public constructor (value?: any, index?: number) { + super(Types, value, index); + + Object.keys(this._def).forEach((_key): void => { + const name = stringUpperFirst(stringCamelCase(_key.replace(' ', '_'))); + const askey = `as${name}`; + const iskey = `is${name}`; + + // do not clobber existing properties on the object + if (isUndefined((this as any)[iskey])) { + Object.defineProperty(this, iskey, { + enumerable: true, + get: (): boolean => this.type === _key + }); + } + + if (isUndefined((this as any)[askey])) { + Object.defineProperty(this, askey, { + enumerable: true, + get: (): Codec => { + assert((this as any)[iskey], `Cannot convert '${this.type}' via ${askey}`); + + return this.value; + } + }); + } + }); + } + }; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public get encodedLength (): number { + return 1 + this.raw.encodedLength; + } + + /** + * @description The index of the metadata value + */ + public get index (): number { + return this._index; + } + + /** + * @description Checks if the Enum points to a [[Null]] type + */ + public get isNone (): boolean { + return this.isNull; + } + + /** + * @description Checks if the Enum points to a [[Null]] type (deprecated, use isNone) + */ + public get isNull (): boolean { + return this.raw instanceof Null; + } + + /** + * @description The name of the type this enum value represents + */ + public get type (): string { + return Object.keys(this._def)[this._index]; + } + + /** + * @description The value of the enum + */ + public get value (): Codec { + return this.raw; + } + + /** + * @description Compares the value of the input to see if there is a match + */ + public eq (other?: any): boolean { + // cater for the case where we only pass the enum index + if (isNumber(other)) { + return this.toNumber() === other; + } else if (this._isBasic && isString(other)) { + return this.type === other; + } + + // compare the actual wrapper value + return this.value.eq(other); + } + + /** + * @description Returns a hex string representation of the value + */ + public toHex (): string { + return u8aToHex(this.toU8a()); + } + + /** + * @description Converts the Object to JSON, typically used for RPC transfers + */ + public toJSON (): AnyJson { + return this._isBasic + ? this._index + : { [this.type]: this.raw.toJSON() }; + } + + /** + * @description Returns the number representation for the value + */ + public toNumber (): number { + return this._index; + } + + /** + * @description Returns the base runtime type name for this instance + */ + public toRawType (): string { + const _enum = this._isBasic + ? Object.keys(this._def) + : Struct.typesToMap(this._def); + + return JSON.stringify({ _enum }); + } + + /** + * @description Returns the string representation of the value + */ + public toString (): string { + return this.isNull + ? this.type + : JSON.stringify(this.toJSON()); + } + + /** + * @description Encodes the value as a Uint8Array as per the SCALE specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + public toU8a (isBare?: boolean): Uint8Array { + const index = this._indexes[this._index]; + + return u8aConcat( + new Uint8Array([index]), + this.raw.toU8a(isBare) + ); + } +} diff --git a/packages/types/src/codec/EnumType.ts b/packages/types/src/codec/EnumType.ts deleted file mode 100644 index 795076b7dc..0000000000 --- a/packages/types/src/codec/EnumType.ts +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AnyJson, Codec, Constructor, InterfaceTypes } from '../types'; - -import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, stringCamelCase, stringUpperFirst, u8aConcat, u8aToHex } from '@polkadot/util'; - -import Null from '../primitive/Null'; -import { mapToTypeMap } from './utils'; -import Base from './Base'; - -interface EnumConstructor { - new(value?: any, index?: number): T; -} - -type TypesDef = Record; - -interface Decoded { - index: number; - value: Codec; -} - -/** - * @name Enum - * @description - * This implements an enum, that based on the value wraps a different type. It is effectively - * an extension to enum where the value type is determined by the actual index. - */ -// TODO: -// - As per Enum, actually use TS enum -// - It should rather probably extend Enum instead of copying code -export default class Enum extends Base { - private _def: TypesDef; - - private _index: number; - - private _indexes: number[]; - - private _isBasic: boolean; - - public constructor (def: Record | string[], value?: any, index?: number | Enum) { - const defInfo = Enum.extractDef(def); - const decoded = Enum.decodeEnum(defInfo.def, value, index); - - super(decoded.value); - - this._def = defInfo.def; - this._isBasic = defInfo.isBasic; - this._indexes = Object.keys(defInfo.def).map((_, index): number => index); - this._index = this._indexes.indexOf(decoded.index) || 0; - } - - private static extractDef (_def: Record | string[]): { def: TypesDef; isBasic: boolean } { - if (!Array.isArray(_def)) { - const def = mapToTypeMap(_def); - const isBasic = !Object.values(def).some((type): boolean => type !== Null); - - return { - def, - isBasic - }; - } - - return { - def: _def.reduce((def, key): TypesDef => { - def[key] = Null; - - return def; - }, {} as unknown as TypesDef), - isBasic: true - }; - } - - private static decodeEnum (def: TypesDef, value?: any, index?: number | Enum): Decoded { - // If `index` is set, we parse it. - if (index instanceof Enum) { - return Enum.createValue(def, index._index, index.raw); - } else if (isNumber(index)) { - return Enum.createValue(def, index, value); - } - - // Or else, we just look at `value` - return Enum.decodeViaValue(def, value); - } - - private static decodeViaValue (def: TypesDef, value?: any): Decoded { - if (value instanceof Enum) { - return Enum.createValue(def, value._index, value.raw); - } else if (isU8a(value)) { - return Enum.createValue(def, value[0], value.subarray(1)); - } else if (isNumber(value)) { - return Enum.createValue(def, value); - } else if (isString(value)) { - const _str = value.toString(); - - return isHex(_str) - ? Enum.decodeViaValue(def, hexToU8a(_str)) - : Enum.createViaJSON(def, _str); - } else if (isObject(value)) { - const key = Object.keys(value)[0]; - - return Enum.createViaJSON(def, key, value[key]); - } - - // Worst-case scenario, return the first with default - return Enum.createValue(def, 0); - } - - private static createViaJSON (def: TypesDef, key: string, value?: any): Decoded { - // JSON comes in the form of { "": "" }, here we - // additionally force to lower to ensure forward compat - const keys = Object.keys(def).map((k): string => k.toLowerCase()); - const keyLower = key.toLowerCase(); - const index = keys.indexOf(keyLower); - - assert(index !== -1, `Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`); - - return Enum.createValue(def, index, value); - } - - private static createValue (def: TypesDef, index: number = 0, value?: any): Decoded { - const Clazz = Object.values(def)[index]; - - assert(!isUndefined(Clazz), `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`); - - return { - index, - value: new Clazz(value) - }; - } - - public static with (Types: Record | string[]): EnumConstructor { - return class extends Enum { - public constructor (value?: any, index?: number) { - super(Types, value, index); - - Object.keys(this._def).forEach((_key): void => { - const name = stringUpperFirst(stringCamelCase(_key.replace(' ', '_'))); - const askey = `as${name}`; - const iskey = `is${name}`; - - // do not clobber existing properties on the object - if (isUndefined((this as any)[iskey])) { - Object.defineProperty(this, iskey, { - enumerable: true, - get: (): boolean => this.type === _key - }); - } - - if (isUndefined((this as any)[askey])) { - Object.defineProperty(this, askey, { - enumerable: true, - get: (): Codec => { - assert((this as any)[iskey], `Cannot convert '${this.type}' via ${askey}`); - - return this.value; - } - }); - } - }); - } - }; - } - - /** - * @description The length of the value when encoded as a Uint8Array - */ - public get encodedLength (): number { - return 1 + this.raw.encodedLength; - } - - /** - * @description The index of the metadata value - */ - public get index (): number { - return this._index; - } - - /** - * @description Checks if the Enum points to a [[Null]] type - */ - public get isNone (): boolean { - return this.isNull; - } - - /** - * @description Checks if the Enum points to a [[Null]] type (deprecated, use isNone) - */ - public get isNull (): boolean { - return this.raw instanceof Null; - } - - /** - * @description The name of the type this enum value represents - */ - public get type (): string { - return Object.keys(this._def)[this._index]; - } - - /** - * @description The value of the enum - */ - public get value (): Codec { - return this.raw; - } - - /** - * @description Compares the value of the input to see if there is a match - */ - public eq (other?: any): boolean { - // cater for the case where we only pass the enum index - if (isNumber(other)) { - return this.toNumber() === other; - } else if (this._isBasic && isString(other)) { - return this.type === other; - } - - // compare the actual wrapper value - return this.value.eq(other); - } - - /** - * @description Returns a hex string representation of the value - */ - public toHex (): string { - return u8aToHex(this.toU8a()); - } - - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): AnyJson { - return this._isBasic - ? this._index - : { [this.type]: this.raw.toJSON() }; - } - - /** - * @description Returns the number representation for the value - */ - public toNumber (): number { - return this._index; - } - - /** - * @description Returns the base runtime type name for this instance - */ - public toRawType (): string { - const _enum = this._isBasic - ? Object.keys(this._def) - : Object.entries(this._def).reduce((result, [key, Type]): Record => { - result[key] = new Type().toRawType(); - - return result; - }, {} as unknown as Record); - - return JSON.stringify({ _enum }); - } - - /** - * @description Returns the string representation of the value - */ - public toString (): string { - return this.isNull - ? this.type - : JSON.stringify(this.toJSON()); - } - - /** - * @description Encodes the value as a Uint8Array as per the SCALE specifications - * @param isBare true when the value has none of the type-specific prefixes (internal) - */ - public toU8a (isBare?: boolean): Uint8Array { - const index = this._indexes[this._index]; - - return u8aConcat( - new Uint8Array([index]), - this.raw.toU8a(isBare) - ); - } -} diff --git a/packages/types/src/codec/Option.ts b/packages/types/src/codec/Option.ts index 054d8a3a89..ae2ccd14c8 100644 --- a/packages/types/src/codec/Option.ts +++ b/packages/types/src/codec/Option.ts @@ -40,14 +40,18 @@ export default class Option extends Base { } else if (isU8a(value)) { // the isU8a check happens last in the if-tree - since the wrapped value // may be an instance of it, so Type and Option checks go in first - return !value.length || value[0] === 0 - ? new Null() - : new Type(value.subarray(1)); + return Option.decodeOptionU8a(Type, value); } return new Type(value); } + private static decodeOptionU8a (Type: Constructor, value: Uint8Array): Codec { + return !value.length || value[0] === 0 + ? new Null() + : new Type(value.subarray(1)); + } + public static with (Type: Constructor | InterfaceTypes): Constructor> { return class extends Option { public constructor (value?: any) { diff --git a/packages/types/src/codec/Struct.ts b/packages/types/src/codec/Struct.ts index 8dce4cea18..5141fc972b 100644 --- a/packages/types/src/codec/Struct.ts +++ b/packages/types/src/codec/Struct.ts @@ -234,16 +234,20 @@ export default class Struct< }, {} as any); } + public static typesToMap (Types: Record): Record { + return Object.entries(Types).reduce((result, [key, Type]): Record => { + result[key] = new Type().toRawType(); + + return result; + }, {} as unknown as Record); + } + /** * @description Returns the base runtime type name for this instance */ public toRawType (): string { return JSON.stringify( - Object.entries(this._Types).reduce((result, [key, Type]): Record => { - result[key] = new Type().toRawType(); - - return result; - }, {} as unknown as Record) + Struct.typesToMap(this._Types) ); } diff --git a/packages/types/src/codec/index.ts b/packages/types/src/codec/index.ts index 0cce6f7c4e..29a6e2e843 100644 --- a/packages/types/src/codec/index.ts +++ b/packages/types/src/codec/index.ts @@ -11,7 +11,6 @@ export { default as Compact } from './Compact'; // export { default as Date } from './Date'; export { default as Enum } from './Enum'; -export { default as EnumType } from './EnumType'; export { default as Option } from './Option'; export { default as Set } from './Set'; export { default as Struct } from './Struct'; diff --git a/packages/types/src/codec/utils/compareMap.ts b/packages/types/src/codec/utils/compareMap.ts index 2fafeb8be0..bcc506ce29 100644 --- a/packages/types/src/codec/utils/compareMap.ts +++ b/packages/types/src/codec/utils/compareMap.ts @@ -13,15 +13,17 @@ function hasMismatch (a?: any, b?: any): boolean { ); } +function notEntry (value: any): boolean { + return !Array.isArray(value) || value.length !== 2; +} + // NOTE These are used internally and when comparing objects, expects that // when the second is an Map that the first has to be as well export default function compareMap (a: Map, b?: any): boolean { if (Array.isArray(b)) { // equal number of entries and each entry in the array should match return (a.size === b.length) && !b.some((entry): boolean => - !Array.isArray(entry) || - entry.length !== 2 || - hasMismatch(a.get(entry[0]), entry[1]) + notEntry(entry) || hasMismatch(a.get(entry[0]), entry[1]) ); } else if (b instanceof Map) { return compareMap(a, [...b.entries()]); diff --git a/packages/types/src/interfaces/babe/definitions.ts b/packages/types/src/interfaces/babe/definitions.ts index 56c25c0076..62b7fffa97 100644 --- a/packages/types/src/interfaces/babe/definitions.ts +++ b/packages/types/src/interfaces/babe/definitions.ts @@ -15,14 +15,14 @@ export default { } }, RawBabePreDigestPrimary: { - authorityIndex: 'u32', // AuthorityIndex (also in aura, not same size there) + authorityIndex: 'u32', // AuthorityIndex (also in aura) slotNumber: 'SlotNumber', weight: 'BabeBlockWeight', vrfOutput: 'H256', // should be '[u8; 32]' (generator support lacking here) vrfProof: 'H256' // should be '[u8; 32]' }, RawBabePreDigestSecondary: { - authorityIndex: 'u32', // AuthorityIndex (also in aura, not same size there) + authorityIndex: 'u32', // AuthorityIndex (also in aura) slotNumber: 'SlotNumber', weight: 'BabeBlockWeight' },