From 49a034a1a0682e856cb7fb0ef0e255c0a5a212c8 Mon Sep 17 00:00:00 2001 From: Jaco Date: Thu, 18 Nov 2021 11:41:27 +0100 Subject: [PATCH] Place u8a decoding top-of-tree (#4230) * Place u8a decoding top-of-tree * Adjust --- packages/types/src/codec/AbstractInt.ts | 26 +++++++--------- packages/types/src/codec/Enum.ts | 18 ++++++----- packages/types/src/codec/Option.ts | 33 +++++++++++--------- packages/types/src/codec/Struct.ts | 40 ++++++------------------- packages/types/src/codec/UInt.spec.ts | 6 ++++ packages/types/src/primitive/Null.ts | 3 ++ 6 files changed, 59 insertions(+), 67 deletions(-) diff --git a/packages/types/src/codec/AbstractInt.ts b/packages/types/src/codec/AbstractInt.ts index b5be1ab47a..4e980dec0c 100644 --- a/packages/types/src/codec/AbstractInt.ts +++ b/packages/types/src/codec/AbstractInt.ts @@ -6,7 +6,7 @@ import type { CodecHash, Hash } from '../interfaces/runtime'; import type { AnyNumber, INumber, Registry } from '../types'; import type { UIntBitLength } from './types'; -import { assert, BN, BN_BILLION, BN_HUNDRED, BN_MILLION, BN_QUINTILL, BN_ZERO, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBn, isBn, isHex, isU8a, stringify, u8aToBn } from '@polkadot/util'; +import { assert, BN, BN_BILLION, BN_HUNDRED, BN_MILLION, BN_QUINTILL, BN_ZERO, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBn, isBn, isHex, isU8a, u8aToBn } from '@polkadot/util'; export const DEFAULT_UINT_BITS = 64; @@ -28,20 +28,8 @@ function toPercentage (value: BN, divisor: BN): string { /** @internal */ function decodeAbstractInt (value: AnyNumber, bitLength: UIntBitLength, isNegative: boolean): string { - // 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 (isU8a(value)) { - if (!value.length) { - return '0'; - } - - try { - // NOTE When passing u8a in (typically from decoded data), it is always Little Endian - return u8aToBn(value.subarray(0, bitLength / 8), { isLe: true, isNegative }).toString(); - } catch (error) { - throw new Error(`AbstractInt: failed on ${stringify(value)}:: ${(error as Error).message}`); - } + return u8aToBn(value.subarray(0, bitLength / 8), { isLe: true, isNegative }).toString(); } else if (isBn(value)) { return value.toString(); } else if (isHex(value, -1, true)) { @@ -68,7 +56,15 @@ export abstract class AbstractInt extends BN implements INumber { readonly #isSigned: boolean; constructor (registry: Registry, value: AnyNumber = 0, bitLength: UIntBitLength = DEFAULT_UINT_BITS, isSigned = false) { - super(decodeAbstractInt(value, bitLength, isSigned)); + // Construct via a string, which will be passed in the BN constructor. + // It would be ideal to actually return a BN, but there is an issue: + // https://github.com/indutny/bn.js/issues/206 + super( + // shortcut isU8a as used in SCALE decoding + isU8a(value) + ? u8aToBn(value.subarray(0, bitLength / 8), { isLe: true, isNegative: isSigned }).toString() + : decodeAbstractInt(value, bitLength, isSigned) + ); this.registry = registry; this.#bitLength = bitLength; diff --git a/packages/types/src/codec/Enum.ts b/packages/types/src/codec/Enum.ts index 5a445fdc9c..3a6d09b5db 100644 --- a/packages/types/src/codec/Enum.ts +++ b/packages/types/src/codec/Enum.ts @@ -164,15 +164,19 @@ export class Enum implements IEnum { readonly #raw: Codec; - constructor (registry: Registry, def: Record | Record | string[], value?: unknown, index?: number) { - const defInfo = extractDef(registry, def); - const decoded = decodeEnum(registry, defInfo.def, value, index); + constructor (registry: Registry, Types: Record | Record | string[], value?: unknown, index?: number) { + const { def, isBasic, isIndexed } = extractDef(registry, Types); + + // shortcut isU8a as used in SCALE decoding + const decoded = isU8a(value) && value.length && !isNumber(index) + ? createFromValue(registry, def, value[0], value.subarray(1)) + : decodeEnum(registry, def, value, index); this.registry = registry; - this.#def = defInfo.def; - this.#isBasic = defInfo.isBasic; - this.#isIndexed = defInfo.isIndexed; - this.#indexes = Object.values(defInfo.def).map(({ index }) => index); + this.#def = def; + this.#isBasic = isBasic; + this.#isIndexed = isIndexed; + this.#indexes = Object.values(def).map(({ index }) => index); this.#entryIndex = this.#indexes.indexOf(decoded.index) || 0; this.#raw = decoded.value; diff --git a/packages/types/src/codec/Option.ts b/packages/types/src/codec/Option.ts index 9bf1f1cbdf..e4c65f1fe8 100644 --- a/packages/types/src/codec/Option.ts +++ b/packages/types/src/codec/Option.ts @@ -5,22 +5,20 @@ import type { HexString } from '@polkadot/util/types'; import type { CodecHash, Hash } from '../interfaces'; import type { AnyJson, Codec, Constructor, IOption, Registry } from '../types'; -import { assert, isNull, isU8a, isUndefined, u8aToHex } from '@polkadot/util'; +import { assert, isCodec, isNull, isU8a, isUndefined, u8aToHex } from '@polkadot/util'; import { Null } from '../primitive/Null'; import { typeToConstructor } from './utils'; /** @internal */ -function decodeOption (registry: Registry, typeName: Constructor | string, value?: unknown): Codec { - if (isNull(value) || isUndefined(value) || value instanceof Null || value === '0x') { - return new Null(registry); +function decodeOption (registry: Registry, Type: Constructor, value?: unknown): Codec { + // In the case of an option, unwrap the inner + if (value instanceof Option) { + value = value.value; } - const Type = typeToConstructor(registry, typeName); - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - if (value instanceof Option) { - return decodeOption(registry, Type, value.value); + if (isNull(value) || isUndefined(value) || value instanceof Null || value === '0x') { + return new Null(registry); } else if (value instanceof Type) { // don't re-create, use as it (which also caters for derived types) return value; @@ -55,12 +53,19 @@ export class Option implements IOption { readonly #raw: T; constructor (registry: Registry, typeName: Constructor | string, value?: unknown) { - this.registry = registry; - this.#Type = typeToConstructor(registry, typeName); - this.#raw = decodeOption(registry, typeName, value) as T; + const Type = typeToConstructor(registry, typeName); + const decoded = isU8a(value) && value.length && !isCodec(value) + ? value[0] === 0 + ? new Null(registry) + : new Type(registry, value.subarray(1)) + : decodeOption(registry, Type, value); - if (this.#raw.initialU8aLength) { - this.#initialU8aLength = 1 + this.#raw.initialU8aLength; + this.registry = registry; + this.#Type = Type; + this.#raw = decoded as T; + + if (decoded.initialU8aLength) { + this.#initialU8aLength = 1 + decoded.initialU8aLength; } } diff --git a/packages/types/src/codec/Struct.ts b/packages/types/src/codec/Struct.ts index 1fb551325a..523ff043c7 100644 --- a/packages/types/src/codec/Struct.ts +++ b/packages/types/src/codec/Struct.ts @@ -12,7 +12,7 @@ import { compareMap, decodeU8a, mapToTypeMap, typesToMap } from './utils'; type TypesDef = Record>; /** @internal */ -function decodeStructFromObject (registry: Registry, Types: ConstructorDef, value: any, jsonMap: Map): Iterable<[string, Codec]> { +function decodeStructFromObject (registry: Registry, Types: ConstructorDef, value: any, jsonMap: Map): [Iterable<[string, Codec]>, number] { let jsonObj: Record | undefined; const inputKeys = Object.keys(Types); const typeofArray = Array.isArray(value); @@ -71,34 +71,7 @@ function decodeStructFromObject (registry: Registry, Types: ConstructorDef, valu } } - return raw; -} - -/** - * Decode input to pass into constructor. - * - * @param Types - Types definition. - * @param value - Value to decode, one of: - * - null - * - undefined - * - hex - * - Uint8Array - * - object with `{ key1: value1, key2: value2 }`, assuming `key1` and `key2` - * are also keys in `Types` - * - array with `[value1, value2]` assuming the array has the same length as - * `Object.keys(Types)` - * @param jsonMap - * @internal - */ -function decodeStruct (registry: Registry, Types: ConstructorDef, value: unknown, jsonMap: Map): [Iterable<[string, Codec]>, number] { - if (isU8a(value) || isHex(value)) { - return decodeU8a(registry, u8aToU8a(value), Types, true); - } else if (value instanceof Struct) { - return [value as Iterable<[string, Codec]>, 0]; - } - - // We assume from here that value is a JS object (Array, Map, Object) - return [decodeStructFromObject(registry, Types, value || {}, jsonMap), 0]; + return [raw, 0]; } /** @@ -128,10 +101,15 @@ export class Struct< readonly #Types: ConstructorDef; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment constructor (registry: Registry, Types: S, value?: V | Map | unknown[] | HexString | null, jsonMap = new Map()) { const typeMap = mapToTypeMap(registry, Types); - const [decoded, decodedLength] = decodeStruct(registry, typeMap, value, jsonMap); + const [decoded, decodedLength] = isU8a(value) + ? decodeU8a(registry, value, typeMap, true) + : isHex(value) + ? decodeU8a(registry, u8aToU8a(value), typeMap, true) + : value instanceof Struct + ? [value as Iterable<[string, Codec]>, 0] + : decodeStructFromObject(registry, typeMap, value || {}, jsonMap); super(decoded); diff --git a/packages/types/src/codec/UInt.spec.ts b/packages/types/src/codec/UInt.spec.ts index cd6c7a55da..73a1294464 100644 --- a/packages/types/src/codec/UInt.spec.ts +++ b/packages/types/src/codec/UInt.spec.ts @@ -11,6 +11,12 @@ import { UInt } from '.'; describe('UInt', (): void => { const registry = new TypeRegistry(); + it('decodes an empty Uint8array correctly', (): void => { + expect( + new UInt(registry, new Uint8Array()).toNumber() + ).toEqual(0); + }); + it('still has the BN interfaces', (): void => { expect([ new UInt(registry, 32).mul(BN_TWO).toNumber(), diff --git a/packages/types/src/primitive/Null.ts b/packages/types/src/primitive/Null.ts index 8157e82142..40ca936d36 100644 --- a/packages/types/src/primitive/Null.ts +++ b/packages/types/src/primitive/Null.ts @@ -17,6 +17,9 @@ export class Null implements Codec { public createdAtHash?: Hash; + // Added for compatibility reasons, e.g. see Option + public readonly initialU8aLength = 0; + constructor (registry: Registry) { this.registry = registry; }