@polkadot/types-codec
Version:
Implementation of the SCALE codec
370 lines (369 loc) • 12.5 kB
JavaScript
import { identity, isHex, isNumber, isObject, isString, isU8a, objectProperties, stringCamelCase, stringify, stringPascalCase, u8aConcatStrict, u8aToHex, u8aToU8a } from '@polkadot/util';
import { mapToTypeMap, typesToMap } from '../utils/index.js';
import { Null } from './Null.js';
function isRustEnum(def) {
const defValues = Object.values(def);
if (defValues.some((v) => isNumber(v))) {
if (!defValues.every((v) => isNumber(v) && v >= 0 && v <= 255)) {
throw new Error('Invalid number-indexed enum definition');
}
return false;
}
return true;
}
function extractDef(registry, _def) {
const def = {};
let isBasic;
let isIndexed;
if (Array.isArray(_def)) {
for (let i = 0, count = _def.length; i < count; i++) {
def[_def[i]] = { Type: Null, index: i };
}
isBasic = true;
isIndexed = false;
}
else if (isRustEnum(_def)) {
const [Types, keys] = mapToTypeMap(registry, _def);
for (let i = 0, count = keys.length; i < count; i++) {
def[keys[i]] = { Type: Types[i], index: i };
}
isBasic = !Object.values(def).some(({ Type }) => Type !== Null);
isIndexed = false;
}
else {
const entries = Object.entries(_def);
for (let i = 0, count = entries.length; i < count; i++) {
const [key, index] = entries[i];
def[key] = { Type: Null, index };
}
isBasic = true;
isIndexed = true;
}
return {
def,
isBasic,
isIndexed
};
}
function getEntryType(def, checkIdx) {
const values = Object.values(def);
for (let i = 0, count = values.length; i < count; i++) {
const { Type, index } = values[i];
if (index === checkIdx) {
return Type;
}
}
throw new Error(`Unable to create Enum via index ${checkIdx}, in ${Object.keys(def).join(', ')}`);
}
function createFromU8a(registry, def, index, value) {
const Type = getEntryType(def, index);
return {
index,
value: new Type(registry, value)
};
}
function createFromValue(registry, def, index = 0, value) {
const Type = getEntryType(def, index);
return {
index,
value: value instanceof Type
? value
: new Type(registry, value)
};
}
function decodeFromJSON(registry, def, key, value) {
// JSON comes in the form of { "<type (camelCase)>": "<value for type>" }, here we
// additionally force to lower to ensure forward compat
const keys = Object.keys(def).map((k) => k.toLowerCase());
const keyLower = key.toLowerCase();
const index = keys.indexOf(keyLower);
if (index === -1) {
throw new Error(`Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`);
}
try {
return createFromValue(registry, def, Object.values(def)[index].index, value);
}
catch (error) {
throw new Error(`Enum(${key}):: ${error.message}`);
}
}
function decodeEnum(registry, def, value, index) {
// NOTE We check the index path first, before looking at values - this allows treating
// the optional indexes before anything else, more-specific > less-specific
if (isNumber(index)) {
return createFromValue(registry, def, index, value);
}
else if (isU8a(value) || isHex(value)) {
const u8a = u8aToU8a(value);
// nested, we don't want to match isObject below
if (u8a.length) {
return createFromU8a(registry, def, u8a[0], u8a.subarray(1));
}
}
else if (value instanceof Enum) {
return createFromValue(registry, def, value.index, value.value);
}
else if (isNumber(value)) {
return createFromValue(registry, def, value);
}
else if (isString(value)) {
return decodeFromJSON(registry, def, value.toString());
}
else if (isObject(value)) {
const key = Object.keys(value)[0];
return decodeFromJSON(registry, def, key, value[key]);
}
// Worst-case scenario, return the first with default
return createFromValue(registry, def, Object.values(def)[0].index);
}
/**
* @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.
*/
export class Enum {
registry;
createdAtHash;
initialU8aLength;
isStorageFallback;
__internal__def;
__internal__entryIndex;
__internal__indexes;
__internal__isBasic;
__internal__isIndexed;
__internal__raw;
constructor(registry, Types, value, index, { definition, setDefinition = identity } = {}) {
const { def, isBasic, isIndexed } = definition || setDefinition(extractDef(registry, Types));
// shortcut isU8a as used in SCALE decoding
const decoded = isU8a(value) && value.length && !isNumber(index)
? createFromU8a(registry, def, value[0], value.subarray(1))
: decodeEnum(registry, def, value, index);
this.registry = registry;
this.__internal__def = def;
this.__internal__isBasic = isBasic;
this.__internal__isIndexed = isIndexed;
this.__internal__indexes = Object.values(def).map(({ index }) => index);
this.__internal__entryIndex = this.__internal__indexes.indexOf(decoded.index);
this.__internal__raw = decoded.value;
if (this.__internal__raw.initialU8aLength) {
this.initialU8aLength = 1 + this.__internal__raw.initialU8aLength;
}
}
static with(Types) {
let definition;
// eslint-disable-next-line no-return-assign
const setDefinition = (d) => definition = d;
return class extends Enum {
static {
const keys = Array.isArray(Types)
? Types
: Object.keys(Types);
const count = keys.length;
const asKeys = new Array(count);
const isKeys = new Array(count);
for (let i = 0; i < count; i++) {
const name = stringPascalCase(keys[i]);
asKeys[i] = `as${name}`;
isKeys[i] = `is${name}`;
}
objectProperties(this.prototype, isKeys, (_, i, self) => self.type === keys[i]);
objectProperties(this.prototype, asKeys, (k, i, self) => {
if (self.type !== keys[i]) {
throw new Error(`Cannot convert '${self.type}' via ${k}`);
}
return self.value;
});
}
constructor(registry, value, index) {
super(registry, Types, value, index, { definition, setDefinition });
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
get encodedLength() {
return 1 + this.__internal__raw.encodedLength;
}
/**
* @description returns a hash of the contents
*/
get hash() {
return this.registry.hash(this.toU8a());
}
/**
* @description The index of the enum value
*/
get index() {
return this.__internal__indexes[this.__internal__entryIndex];
}
/**
* @description The value of the enum
*/
get inner() {
return this.__internal__raw;
}
/**
* @description true if this is a basic enum (no values)
*/
get isBasic() {
return this.__internal__isBasic;
}
/**
* @description Checks if the value is an empty value
*/
get isEmpty() {
return this.__internal__raw.isEmpty;
}
/**
* @description Checks if the Enum points to a [[Null]] type
*/
get isNone() {
return this.__internal__raw instanceof Null;
}
/**
* @description The available keys for this enum
*/
get defIndexes() {
return this.__internal__indexes;
}
/**
* @description The available keys for this enum
*/
get defKeys() {
return Object.keys(this.__internal__def);
}
/**
* @description The name of the type this enum value represents
*/
get type() {
return this.defKeys[this.__internal__entryIndex];
}
/**
* @description The value of the enum
*/
get value() {
return this.__internal__raw;
}
/**
* @description Compares the value of the input to see if there is a match
*/
eq(other) {
// cater for the case where we only pass the enum index
if (isU8a(other)) {
return !this.toU8a().some((entry, index) => entry !== other[index]);
}
else if (isNumber(other)) {
return this.toNumber() === other;
}
else if (this.__internal__isBasic && isString(other)) {
return this.type === other;
}
else if (isHex(other)) {
return this.toHex() === other;
}
else if (other instanceof Enum) {
return this.index === other.index && this.value.eq(other.value);
}
else if (isObject(other)) {
return this.value.eq(other[this.type]);
}
// compare the actual wrapper value
return this.value.eq(other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
inspect() {
if (this.__internal__isBasic) {
return { outer: [new Uint8Array([this.index])] };
}
const { inner, outer = [] } = this.__internal__raw.inspect();
return {
inner,
outer: [new Uint8Array([this.index]), ...outer]
};
}
/**
* @description Returns a hex string representation of the value
*/
toHex() {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
toHuman(isExtended, disableAscii) {
return this.__internal__isBasic || this.isNone
? this.type
: { [this.type]: this.__internal__raw.toHuman(isExtended, disableAscii) };
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
toJSON() {
return this.__internal__isBasic
? this.type
: { [stringCamelCase(this.type)]: this.__internal__raw.toJSON() };
}
/**
* @description Returns the number representation for the value
*/
toNumber() {
return this.index;
}
/**
* @description Converts the value in a best-fit primitive form
*/
toPrimitive(disableAscii) {
return this.__internal__isBasic
? this.type
: { [stringCamelCase(this.type)]: this.__internal__raw.toPrimitive(disableAscii) };
}
/**
* @description Returns a raw struct representation of the enum types
*/
_toRawStruct() {
if (this.__internal__isBasic) {
return this.__internal__isIndexed
? this.defKeys.reduce((out, key, index) => {
out[key] = this.__internal__indexes[index];
return out;
}, {})
: this.defKeys;
}
const entries = Object.entries(this.__internal__def);
return typesToMap(this.registry, entries.reduce((out, [key, { Type }], i) => {
out[0][i] = Type;
out[1][i] = key;
return out;
}, [new Array(entries.length), new Array(entries.length)]));
}
/**
* @description Returns the base runtime type name for this instance
*/
toRawType() {
return stringify({ _enum: this._toRawStruct() });
}
/**
* @description Returns the string representation of the value
*/
toString() {
return this.isNone
? this.type
: 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)
*/
toU8a(isBare) {
return isBare
? this.__internal__raw.toU8a(isBare)
: u8aConcatStrict([
new Uint8Array([this.index]),
this.__internal__raw.toU8a(isBare)
]);
}
}