// Copyright 2017-2021 @polkadot/types authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { CodecHash, Hash } from '../interfaces/runtime'; import type { AnyJson, Codec, Constructor, InterfaceTypes, Registry } from '../types'; import { compactFromU8a, compactToU8a, isHex, isObject, isU8a, logger, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util'; import { compareMap, decodeU8a, typeToConstructor } from './utils'; const l = logger('Map'); /** @internal */ function decodeMapFromU8a (registry: Registry, KeyClass: Constructor, ValClass: Constructor, u8a: Uint8Array): Map { const output = new Map(); const [offset, length] = compactFromU8a(u8a); const types = []; for (let i = 0; i < length.toNumber(); i++) { types.push(KeyClass, ValClass); } const values = decodeU8a(registry, u8a.subarray(offset), types); for (let i = 0; i < values.length; i += 2) { output.set(values[i] as K, values[i + 1] as V); } return output; } /** @internal */ function decodeMapFromMap (registry: Registry, KeyClass: Constructor, ValClass: Constructor, value: Map): Map { const output = new Map(); value.forEach((val: any, key: any) => { try { output.set( key instanceof KeyClass ? key : new KeyClass(registry, key), val instanceof ValClass ? val : new ValClass(registry, val) ); } catch (error) { l.error('Failed to decode key or value:', (error as Error).message); throw error; } }); return output; } /** * Decode input to pass into constructor. * * @param KeyClass - Type of the map key * @param ValClass - Type of the map value * @param value - Value to decode, one of: * - null * - undefined * - hex * - Uint8Array * - Map, where both key and value types are either * constructors or decodeable values for their types. * @param jsonMap * @internal */ function decodeMap (registry: Registry, keyType: Constructor | keyof InterfaceTypes, valType: Constructor | keyof InterfaceTypes, value?: Uint8Array | string | Map): Map { const KeyClass = typeToConstructor(registry, keyType); const ValClass = typeToConstructor(registry, valType); if (!value) { return new Map(); } else if (isU8a(value) || isHex(value)) { return decodeMapFromU8a(registry, KeyClass, ValClass, u8aToU8a(value)); } else if (value instanceof Map) { return decodeMapFromMap(registry, KeyClass, ValClass, value); } else if (isObject(value)) { return decodeMapFromMap(registry, KeyClass, ValClass, new Map(Object.entries(value))); } throw new Error('Map: cannot decode type'); } export class CodecMap extends Map implements Codec { public readonly registry: Registry; public createdAtHash?: Hash; readonly #KeyClass: Constructor; readonly #ValClass: Constructor; readonly #type: string; constructor (registry: Registry, keyType: Constructor | keyof InterfaceTypes, valType: Constructor | keyof InterfaceTypes, rawValue: Uint8Array | string | Map | undefined, type: 'BTreeMap' | 'HashMap' = 'HashMap') { super(decodeMap(registry, keyType, valType, rawValue)); this.registry = registry; this.#KeyClass = typeToConstructor(registry, keyType); this.#ValClass = typeToConstructor(registry, valType); this.#type = type; } /** * @description The length of the value when encoded as a Uint8Array */ public get encodedLength (): number { let len = compactToU8a(this.size).length; this.forEach((v: V, k: K) => { len += v.encodedLength + k.encodedLength; }); return len; } /** * @description Returns a hash of the value */ public get hash (): CodecHash { return this.registry.hash(this.toU8a()); } /** * @description Checks if the value is an empty value */ public get isEmpty (): boolean { return this.size === 0; } /** * @description Compares the value of the input to see if there is a match */ public eq (other?: unknown): boolean { return compareMap(this, other); } /** * @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation */ public toHex (): string { return u8aToHex(this.toU8a()); } /** * @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information */ public toHuman (isExtended?: boolean): Record { const json: Record = {}; this.forEach((v: V, k: K) => { json[k.toString()] = v.toHuman(isExtended); }); return json; } /** * @description Converts the Object to JSON, typically used for RPC transfers */ public toJSON (): Record { const json: Record = {}; this.forEach((v: V, k: K) => { json[k.toString()] = v.toJSON(); }); return json; } /** * @description Returns the base runtime type name for this instance */ public toRawType (): string { return `${this.#type}<${this.registry.getClassName(this.#KeyClass) || new this.#KeyClass(this.registry).toRawType()},${this.registry.getClassName(this.#ValClass) || new this.#ValClass(this.registry).toRawType()}>`; } /** * @description Returns the string representation of the value */ public toString (): string { return 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 encoded = new Array(); if (!isBare) { encoded.push(compactToU8a(this.size)); } this.forEach((v: V, k: K) => { encoded.push(k.toU8a(isBare), v.toU8a(isBare)); }); return u8aConcat(...encoded); } }