Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle

This commit is contained in:
2026-01-07 02:29:40 +03:00
commit d5f038faea
1383 changed files with 1088018 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# @pezkuwi/types-codec
Base scale-codec types.
+34
View File
@@ -0,0 +1,34 @@
{
"author": "Jaco Greeff <jacogr@gmail.com>",
"bugs": "https://github.com/pezkuwichain/pezkuwi-api/issues",
"description": "Implementation of the SCALE codec",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/pezkuwichain/pezkuwi-api/tree/master/packages/types-codec#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/types-codec",
"repository": {
"directory": "packages/types-codec",
"type": "git",
"url": "https://github.com/pezkuwichain/pezkuwi-api.git"
},
"sideEffects": [
"./packageDetect.js",
"./packageDetect.cjs"
],
"type": "module",
"version": "16.5.6",
"main": "index.js",
"dependencies": {
"@pezkuwi/util": "^14.0.1",
"@pezkuwi/x-bigint": "^14.0.1",
"tslib": "^2.8.1"
},
"devDependencies": {
"@pezkuwi/types": "16.5.4",
"@pezkuwi/types-augment": "16.5.4",
"@pezkuwi/types-support": "16.5.4",
"@pezkuwi/util-crypto": "^14.0.1"
}
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, Inspect, IU8a, IVec, Registry } from '../types/index.js';
import { compactToU8a, u8aConcatStrict, u8aToHex } from '@pezkuwi/util';
import { compareArray } from '../utils/compareArray.js';
/**
* @name AbstractArray
* @description
* This manages codec arrays. It is an extension to Array, providing
* specific encoding/decoding on top of the base type.
* @noInheritDoc
*/
export abstract class AbstractArray<T extends Codec> extends Array<T> implements IVec<T> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
/**
* @description This ensures that operators such as clice, filter, map, etc. return
* new Array instances (without this we need to apply overrides)
*/
static get [Symbol.species] (): typeof Array {
return Array;
}
protected constructor (registry: Registry, length: number) {
super(length);
this.registry = registry;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
// We need to loop through all entries since they may have a variable length themselves,
// e.g. when a Vec or Compact is contained withing, it has a variable length based on data
const count = this.length;
let total = compactToU8a(count).length;
for (let i = 0; i < count; i++) {
total += this[i].encodedLength;
}
return total;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.length === 0;
}
/**
* @description The length of the value
*/
public override get length (): number {
// only included here since we ignore inherited docs
return super.length;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return compareArray(this, other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
inner: this.inspectInner(),
outer: [compactToU8a(this.length)]
};
}
/**
* @internal
* @description Internal per-item inspection of internal values
*/
public inspectInner (): Inspect[] {
const count = this.length;
const inner = new Array<Inspect>(count);
for (let i = 0; i < count; i++) {
inner[i] = this[i].inspect();
}
return inner;
}
/**
* @description Converts the Object to an standard JavaScript Array
*/
public toArray (): T[] {
return Array.from(this);
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
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, disableAscii?: boolean): AnyJson {
const count = this.length;
const result = new Array<AnyJson>(count);
for (let i = 0; i < count; i++) {
result[i] = this[i] && this[i].toHuman(isExtended, disableAscii);
}
return result;
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
const count = this.length;
const result = new Array<AnyJson>(count);
for (let i = 0; i < count; i++) {
// We actually log inside the U8a decoding and use JSON.stringify(...), which
// means that the Vec may be partially populated (same applies to toHuman, same check)
result[i] = this[i] && this[i].toJSON();
}
return result;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
const count = this.length;
const result = new Array<AnyJson>(count);
for (let i = 0; i < count; i++) {
result[i] = this[i] && this[i].toPrimitive(disableAscii);
}
return result;
}
/**
* @description Returns the base runtime type name for this instance
*/
abstract toRawType (): string;
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
const count = this.length;
const result = new Array<string>(count);
for (let i = 0; i < count; i++) {
result[i] = this[i].toString();
}
return `[${result.join(', ')}]`;
}
/**
* @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 = this.toU8aInner();
return isBare
? u8aConcatStrict(encoded)
: u8aConcatStrict([compactToU8a(this.length), ...encoded]);
}
/**
* @internal
* @description Internal per-item SCALE encoding of contained values
* @param isBare true when the value has none of the type-specific prefixes (internal)
*/
public toU8aInner (isBare?: boolean): Uint8Array[] {
const count = this.length;
const encoded = new Array<Uint8Array>(count);
for (let i = 0; i < count; i++) {
encoded[i] = this[i].toU8a(isBare);
}
return encoded;
}
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, BareOpts, Codec, Inspect, IU8a, Registry } from '../types/index.js';
/**
* @name Base
* @description A type extends the Base class, when it holds a value
*/
export abstract class AbstractBase<T extends Codec> implements Codec {
readonly registry: Registry;
public createdAtHash?: IU8a | undefined;
public initialU8aLength?: number | undefined;
public isStorageFallback?: boolean;
readonly #raw: T;
protected constructor (registry: Registry, value: T, initialU8aLength?: number) {
this.initialU8aLength = initialU8aLength;
this.#raw = value;
this.registry = registry;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.toU8a().length;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description returns the inner (wrapped value)
*/
public get inner (): T {
return this.#raw;
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.#raw.isEmpty;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return this.#raw.eq(other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return this.#raw.inspect();
}
/**
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
*/
public toHex (isLe?: boolean): HexString {
return this.#raw.toHex(isLe);
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson {
return this.#raw.toHuman(isExtended, disableAscii);
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this.#raw.toJSON();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
return this.#raw.toPrimitive(disableAscii);
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.#raw.toString();
}
/**
* @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?: BareOpts): Uint8Array {
return this.#raw.toU8a(isBare);
}
/**
* @description Returns the base runtime type name for this instance
*/
public abstract toRawType (): string;
/**
* @description Returns the inner wrapped value (equivalent to valueOf)
*/
public unwrap (): T {
return this.#raw;
}
/**
* @description Returns the inner wrapped value
*/
public valueOf (): T {
return this.#raw;
}
}
+271
View File
@@ -0,0 +1,271 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyNumber, Inspect, INumber, IU8a, Registry, ToBn, UIntBitLength } from '../types/index.js';
import { BN, BN_BILLION, BN_HUNDRED, BN_MILLION, BN_QUINTILL, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBn, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, u8aToBn, u8aToNumber } from '@pezkuwi/util';
export const DEFAULT_UINT_BITS = 64;
// Maximum allowed integer for JS is 2^53 - 1, set limit at 52
// In this case however, we always print any >32 as hex
const MAX_NUMBER_BITS = 52;
const MUL_P = new BN(1_00_00);
const FORMATTERS: [string, BN][] = [
['Perquintill', BN_QUINTILL],
['Perbill', BN_BILLION],
['Permill', BN_MILLION],
['Percent', BN_HUNDRED]
];
function isToBn (value: unknown): value is ToBn {
return isFunction((value as ToBn).toBn);
}
function toPercentage (value: BN, divisor: BN): string {
return `${(value.mul(MUL_P).div(divisor).toNumber() / 100).toFixed(2)}%`;
}
/** @internal */
function decodeAbstractInt (value: Exclude<AnyNumber, Uint8Array> | Record<string, string> | ToBn | null, isNegative: boolean): string | number {
if (isNumber(value)) {
if (!Number.isInteger(value) || value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
throw new Error('Number needs to be an integer <= Number.MAX_SAFE_INTEGER, i.e. 2 ^ 53 - 1');
}
return value;
} else if (isString(value)) {
if (isHex(value, -1, true)) {
return hexToBn(value, { isLe: false, isNegative }).toString();
}
if (value.includes('.') || value.includes(',') || value.includes('e')) {
throw new Error('String should not contain decimal points or scientific notation');
}
return value;
} else if (isBn(value) || isBigInt(value)) {
return value.toString();
} else if (isObject(value)) {
if (isToBn(value)) {
return value.toBn().toString();
}
// Allow the construction from an object with a single top-level key. This means that
// single key objects can be treated equivalently to numbers, assuming they meet the
// specific requirements. (This is useful in Weights 1.5 where Objects are compact)
const keys = Object.keys(value);
if (keys.length !== 1) {
throw new Error('Unable to construct number from multi-key object');
}
return decodeAbstractInt(value[keys[0]], isNegative);
} else if (!value) {
return 0;
}
throw new Error(`Unable to create BN from unknown type ${typeof value}`);
}
/**
* @name AbstractInt
* @ignore
* @noInheritDoc
*/
export abstract class AbstractInt extends BN implements INumber {
readonly registry: Registry;
readonly encodedLength: number;
readonly isUnsigned: boolean;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #bitLength: UIntBitLength;
constructor (registry: Registry, value: AnyNumber | null = 0, bitLength: UIntBitLength = DEFAULT_UINT_BITS, isSigned = false) {
// Construct via a string/number, 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)
? bitLength <= 48
? u8aToNumber(value.subarray(0, bitLength / 8), { isNegative: isSigned })
: u8aToBn(value.subarray(0, bitLength / 8), { isLe: true, isNegative: isSigned }).toString()
: decodeAbstractInt(value, isSigned)
);
this.registry = registry;
this.#bitLength = bitLength;
this.encodedLength = this.#bitLength / 8;
this.initialU8aLength = this.#bitLength / 8;
this.isUnsigned = !isSigned;
const isNegative = this.isNeg();
const maxBits = bitLength - (isSigned && !isNegative ? 1 : 0);
if (isNegative && !isSigned) {
throw new Error(`${this.toRawType()}: Negative number passed to unsigned type`);
} else if (super.bitLength() > maxBits) {
throw new Error(`${this.toRawType()}: Input too large. Found input with ${super.bitLength()} bits, expected ${maxBits}`);
}
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is a zero value (align elsewhere)
*/
public get isEmpty (): boolean {
return this.isZero();
}
/**
* @description Returns the number of bits in the value
*/
public override bitLength (): number {
return this.#bitLength;
}
/**
* @description Compares the value of the input to see if there is a match
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public override eq (other?: unknown): boolean {
// Here we are actually overriding the built-in .eq to take care of both
// number and BN inputs (no `.eqn` needed) - numbers will be converted
return super.eq(
isHex(other)
? hexToBn(other.toString(), { isLe: false, isNegative: !this.isUnsigned })
: bnToBn(other as string)
);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description True if this value is the max of the type
*/
public isMax (): boolean {
const u8a = this.toU8a().filter((b) => b === 0xff);
return u8a.length === (this.#bitLength / 8);
}
/**
* @description Returns a BigInt representation of the number
*/
public toBigInt (): bigint {
return BigInt(this.toString());
}
/**
* @description Returns the BN representation of the number. (Compatibility)
*/
public toBn (): BN {
return this;
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (isLe = false): HexString {
// For display/JSON, this is BE, for compare, use isLe
return bnToHex(this, {
bitLength: this.bitLength(),
isLe,
isNegative: !this.isUnsigned
});
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (_isExpanded?: boolean): string {
const rawType = this.toRawType();
if (rawType === 'Balance') {
return this.isMax()
? 'everything'
// FIXME In the case of multiples we need some way of detecting which instance this belongs
// to. as it stands we will always format (incorrectly) against the first token defined
: formatBalance(this, { decimals: this.registry.chainDecimals[0], withSi: true, withUnit: this.registry.chainTokens[0] });
}
const [, divisor] = FORMATTERS.find(([type]) => type === rawType) || [];
return divisor
? toPercentage(this, divisor)
: formatNumber(this);
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public override toJSON (onlyHex = false): any {
// FIXME this return type should by string | number, however BN returns string
// Options here are
// - super.bitLength() - the actual used bits, use hex when close to MAX_SAFE_INTEGER
// - this.#bitLength - the max used bits, use hex when larger than native Rust type
return onlyHex || (this.#bitLength > 128) || (super.bitLength() > MAX_NUMBER_BITS)
? this.toHex()
: this.toNumber();
}
/**
* @description Returns the value in a primitive form, either number when <= 52 bits, or string otherwise
*/
public toPrimitive (): number | string {
return super.bitLength() > MAX_NUMBER_BITS
? this.toString()
: this.toNumber();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
// NOTE In the case of balances, which have a special meaning on the UI
// and can be interpreted differently, return a specific value for it so
// underlying it always matches (no matter which length it actually is)
return this instanceof this.registry.createClassUnsafe('Balance')
? 'Balance'
: `${this.isUnsigned ? 'u' : 'i'}${this.bitLength()}`;
}
/**
* @description Returns the string representation of the value
* @param base The base to use for the conversion
*/
public override toString (base?: number): string {
// only included here since we do not inherit docs
return super.toString(base);
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return bnToU8a(this, {
bitLength: this.bitLength(),
isLe: true,
isNegative: !this.isUnsigned
});
}
}
@@ -0,0 +1,99 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, BareOpts, CodecObject, Inspect, IU8a, Registry, ToString } from '../types/index.js';
/**
* @name Object
* @description A type extends the Base class, when it holds a value
*/
export abstract class AbstractObject<T extends ToString> implements CodecObject<T> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number | undefined;
public isStorageFallback?: boolean;
readonly $: T;
protected constructor (registry: Registry, value: T, initialU8aLength?: number) {
this.$ = value;
this.initialU8aLength = initialU8aLength;
this.registry = registry;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.toU8a().length;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public abstract get isEmpty (): boolean;
/**
* @description Compares the value of the input to see if there is a match
*/
public abstract eq (other?: unknown): boolean;
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public abstract inspect (): Inspect;
/**
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
*/
public abstract toHex (isLe?: boolean): HexString;
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public abstract toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson;
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public abstract toJSON (): AnyJson;
/**
* @description Converts the value in a best-fit primitive form
*/
public abstract toPrimitive (disableAscii?: boolean): AnyJson;
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.$.toString();
}
/**
* @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 abstract toU8a (isBare?: BareOpts): Uint8Array;
/**
* @description Returns the base runtime type name for this instance
*/
public abstract toRawType (): string;
/**
* @description Return the internal value (JS-aligned, same result as $)
*/
public valueOf (): T {
return this.$;
}
}
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { AbstractArray } from './Array.js';
export { AbstractBase } from './Base.js';
export { AbstractInt } from './Int.js';
@@ -0,0 +1,99 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { CodecDate, Compact, U32 } from '@pezkuwi/types-codec';
import { BN } from '@pezkuwi/util';
import { perf } from '../test/performance.js';
const CompactU32 = Compact.with(U32);
describe('Compact', (): void => {
const registry = new TypeRegistry();
describe('constructor', (): void => {
it('fails on > MAX_SAFE_INTEGER', (): void => {
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision, no-loss-of-precision
expect(() => new Compact(registry, 'u128', 9007199254740999)).toThrow(/integer <= Number.MAX_SAFE_INTEGER/);
});
it('has support for BigInt', (): void => {
expect(
new Compact(registry, 'u128', 123456789000123456789n).toHuman()
).toEqual('123,456,789,000,123,456,789');
});
it('has the correct bitLength for constructor values (BlockNumber)', (): void => {
expect(
new (Compact.with(registry.createClass('BlockNumber')))(registry, 0xfffffff9).bitLength()
).toEqual(32);
});
it('has the correct encodedLength for constructor values (string BlockNumber)', (): void => {
expect(
new (Compact.with('BlockNumber'))(registry, 0xfffffff9).encodedLength
).toEqual(5);
});
it('has the correct encodedLength for constructor values (class BlockNumber)', (): void => {
expect(
new (Compact.with(registry.createClass('BlockNumber')))(registry, 0xfffffff9).encodedLength
).toEqual(5);
});
it('has the correct encodedLength for constructor values (u32)', (): void => {
expect(
new (Compact.with(U32))(registry, 0xffff9).encodedLength
).toEqual(4);
});
it('constructs properly via Uint8Array as U32', (): void => {
expect(
new (Compact.with(U32))(registry, new Uint8Array([254, 255, 3, 0])).toNumber()
).toEqual(new BN(0xffff).toNumber());
});
it('constructs properly via number as Moment', (): void => {
expect(
new (Compact.with(CodecDate))(registry, 1537968546).toString().startsWith('Wed Sep 26 2018') // The time depends on the timezone this test is run in
).toBe(true);
});
});
describe('utils', (): void => {
it('compares against another Compact', (): void => {
expect(new (Compact.with(U32))(registry, 12345).eq(new (Compact.with(U32))(registry, 12345))).toBe(true);
});
it('compares against a primitive', (): void => {
expect(new (Compact.with(U32))(registry, 12345).eq(12345)).toBe(true);
});
it('unwraps to the wrapped value', (): void => {
expect(new (Compact.with(U32))(registry, 12345).unwrap() instanceof U32).toBe(true);
});
it('has a valid toBn interface', (): void => {
expect(new (Compact.with('u128'))(registry, '12345678987654321').toBn().eq(new BN('12345678987654321'))).toBe(true);
});
it('has a valid toBigInt interface', (): void => {
expect(
(new (Compact.with('u128'))(registry, 12345678987654321n).toBigInt() + 1n) === 12345678987654322n
).toBe(true);
});
it('has a sane inspect', (): void => {
expect(
new (Compact.with(U32))(registry, 0xffff).inspect()
).toEqual({
outer: [new Uint8Array([254, 255, 3, 0])]
});
});
});
perf('Compact', 75_000, [[new Uint8Array([63 << 2])]], (v: Uint8Array) => new CompactU32(registry, v));
});
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, AnyNumber, CodecClass, DefinitionSetter, ICompact, Inspect, INumber, IU8a, Registry } from '../types/index.js';
import { compactFromU8a, compactFromU8aLim, compactToU8a, identity, isU8a } from '@pezkuwi/util';
import { typeToConstructor } from '../utils/index.js';
function decodeCompact<T extends INumber> (registry: Registry, Type: CodecClass<T>, value: Compact<T> | AnyNumber): [T, number] {
if (isU8a(value)) {
const [decodedLength, bn] = (value[0] & 0b11) < 0b11
? compactFromU8aLim(value)
: compactFromU8a(value);
return [new Type(registry, bn), decodedLength];
} else if (value instanceof Compact) {
const raw = value.unwrap();
return raw instanceof Type
? [raw, 0]
: [new Type(registry, raw), 0];
} else if (value instanceof Type) {
return [value, 0];
}
return [new Type(registry, value), 0];
}
/**
* @name Compact
* @description
* A compact length-encoding codec wrapper. It performs the same function as Length, however
* differs in that it uses a variable number of bytes to do the actual encoding. This is mostly
* used by other types to add length-prefixed encoding, or in the case of wrapped types, taking
* a number and making the compact representation thereof
*/
export class Compact<T extends INumber> implements ICompact<T> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #Type: CodecClass<T>;
readonly #raw: T;
constructor (registry: Registry, Type: CodecClass<T> | string, value: Compact<T> | AnyNumber = 0, { definition, setDefinition = identity }: DefinitionSetter<CodecClass<T>> = {}) {
this.registry = registry;
this.#Type = definition || setDefinition(typeToConstructor(registry, Type));
const [raw, decodedLength] = decodeCompact<T>(registry, this.#Type, value);
this.initialU8aLength = decodedLength;
this.#raw = raw;
}
public static with<O extends INumber> (Type: CodecClass<O> | string): CodecClass<Compact<O>> {
let definition: CodecClass<O> | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = <T> (d: CodecClass<T>) =>
(definition = d as unknown as CodecClass<O>) as unknown as CodecClass<T>;
return class extends Compact<O> {
constructor (registry: Registry, value?: Compact<O> | AnyNumber) {
super(registry, Type, value, { definition, setDefinition });
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.toU8a().length;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.#raw.isEmpty;
}
/**
* @description Returns the number of bits in the value
*/
public bitLength (): number {
return this.#raw.bitLength();
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return this.#raw.eq(
other instanceof Compact
? other.#raw
: other
);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a BigInt representation of the number
*/
public toBigInt (): bigint {
return this.#raw.toBigInt();
}
/**
* @description Returns the BN representation of the number
*/
public toBn (): BN {
return this.#raw.toBn();
}
/**
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
*/
public toHex (isLe?: boolean): HexString {
return this.#raw.toHex(isLe);
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson {
return this.#raw.toHuman(isExtended, disableAscii);
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this.#raw.toJSON();
}
/**
* @description Returns the number representation for the value
*/
public toNumber (): number {
return this.#raw.toNumber();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): string | number {
return this.#raw.toPrimitive(disableAscii);
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return `Compact<${this.registry.getClassName(this.#Type) || this.#raw.toRawType()}>`;
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.#raw.toString();
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return compactToU8a(this.#raw.toBn());
}
/**
* @description Returns the embedded [[UInt]] or [[Moment]] value
*/
public unwrap (): T {
return this.#raw;
}
}
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { DoNotConstruct } from '@pezkuwi/types-codec';
describe('DoNotConstruct', (): void => {
const registry = new TypeRegistry();
it('does not allow construction', (): void => {
expect(
() => new (DoNotConstruct.with())(registry)
).toThrow(/Cannot construct unknown type DoNotConstruct/);
});
it('does not allow construction (with Name)', (): void => {
expect(
() => new (DoNotConstruct.with('Something'))(registry)
).toThrow(/Cannot construct unknown type Something/);
});
});
@@ -0,0 +1,118 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, CodecClass, Inspect, IU8a, Registry } from '../types/index.js';
/**
* @name DoNotConstruct
* @description
* An unknown type that fails on construction with the type info
*/
export class DoNotConstruct implements Codec {
readonly registry: Registry;
public createdAtHash?: IU8a;
public isStorageFallback?: boolean;
#neverError: Error;
constructor (registry: Registry, typeName = 'DoNotConstruct') {
this.registry = registry;
this.#neverError = new Error(`DoNotConstruct: Cannot construct unknown type ${typeName}`);
throw this.#neverError;
}
public static with (typeName?: string): CodecClass {
return class extends DoNotConstruct {
constructor (registry: Registry) {
super(registry, typeName);
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
throw this.#neverError;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
throw this.#neverError;
}
/**
* @description Checks if the value is an empty value (always true)
*/
public get isEmpty (): boolean {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
eq (): boolean {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
public inspect (): Inspect {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toHex (): HexString {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toHuman (): AnyJson {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toJSON (): AnyJson {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toPrimitive (): AnyJson {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toRawType (): string {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toString (): string {
throw this.#neverError;
}
/**
* @description Unimplemented
*/
toU8a (): Uint8Array {
throw this.#neverError;
}
}
+487
View File
@@ -0,0 +1,487 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { Registry } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { Enum, Null, Text, U32 } from '@pezkuwi/types-codec';
import { stringify, u8aToHex } from '@pezkuwi/util';
import { perf } from '../test/performance.js';
const PEnum = Enum.with({ a: U32, b: U32 });
describe('Enum', (): void => {
const registry = new TypeRegistry();
describe('typed enum (previously EnumType)', (): void => {
it('provides a clean toString() (value)', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
new Uint8Array([0, 2 << 2, 49, 50])
).value.toString()
).toEqual('12');
});
it('provides a clean toString() (enum)', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
new Uint8Array([1, 2 << 2, 49, 50])
).toString()
).toEqual('{"u32":3289352}');
});
it('decodes from a JSON input (lowercase)', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
{ text: 'some text value' }
).value.toString()
).toEqual('some text value');
});
it('decodes reusing instantiated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
new Enum(
registry,
{ foo: Text },
{ foo }
).value
).toBe(foo);
expect(
new Enum(
registry,
{ foo: Text },
foo,
0
).value
).toBe(foo);
expect(
new Enum(
registry,
{ foo: Text },
new Enum(registry, { foo: Text }, { foo })
).value
).toBe(foo);
});
it('decodes from hex', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
'0x0134120000'
).value.toString()
).toEqual('4660'); // 0x1234 in decimal
});
it('decodes from hex (string types)', (): void => {
expect(
new Enum(
registry,
// eslint-disable-next-line sort-keys
{ foo: 'Text', bar: 'u32' },
'0x0134120000'
).value.toString()
).toEqual('4660'); // 0x1234 in decimal
});
it('decodes from a JSON input (mixed case)', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
{ U32: 42 }
).value.toString()
).toEqual('42');
});
it('decodes from JSON string', (): void => {
expect(
new Enum(
registry,
{ Null, U32 },
'null'
).type
).toEqual('Null');
});
it('has correct isXyz/asXyz (Enum.with)', (): void => {
const test = new (Enum.with({ First: Text, Second: U32, Third: U32 }))(registry, { Second: 42 }) as any as { isFirst: boolean; isSecond: boolean; asSecond: U32; isThird: boolean; asThird: never };
// const asKeys = Object.keys(test).filter((k) => k.startsWith('as'));
// const isKeys = Object.keys(test).filter((k) => k.startsWith('is'));
// expect([isKeys, asKeys]).toEqual([
// ['isFirst', 'isSecond', 'isThird'],
// ['asFirst', 'asSecond', 'asThird']
// ]);
expect([test.isFirst, test.isSecond, test.isThird]).toEqual([false, true, false]);
expect(test.asSecond.toNumber()).toEqual(42);
expect((): never => test.asThird).toThrow(/Cannot convert 'Second' via asThird/);
});
it('stringifies with custom types', (): void => {
class A extends Null { }
class B extends Null { }
class C extends Null { }
class Test extends Enum {
constructor (registry: Registry, value?: string, index?: number) {
super(registry, {
a: A,
b: B,
c: C
}, value, index);
}
}
expect(new Test(registry).toJSON()).toEqual({ a: null });
});
it('creates via with', (): void => {
class A extends Null { }
class B extends U32 { }
class C extends Null { }
const Test = Enum.with({ A, B, C });
expect(new Test(registry).toJSON()).toEqual({ a: null });
expect(new Test(registry, 1234, 1).toJSON()).toEqual({ b: 1234 });
expect(new Test(registry, 0x1234, 1).toU8a()).toEqual(new Uint8Array([1, 0x34, 0x12, 0x00, 0x00]));
expect(new Test(registry, 0x1234, 1).toU8a(true)).toEqual(new Uint8Array([0x34, 0x12, 0x00, 0x00]));
});
it('allows accessing the type and value', (): void => {
const text = new Text(registry, 'foo');
const enumType = new Enum(
registry,
{ Text, U32 },
{ Text: text }
);
expect(enumType.type).toBe('Text');
expect(enumType.value).toEqual(text);
});
describe('utils', (): void => {
const DEF = { num: U32, str: Text };
const u8a = new Uint8Array([1, 3 << 2, 88, 89, 90]);
const test = new Enum(registry, DEF, u8a);
it('compares against index', (): void => {
expect(test.eq(1)).toBe(true);
});
it('compares against u8a', (): void => {
expect(test.eq(u8a)).toBe(true);
});
it('compares against hex', (): void => {
expect(test.eq(u8aToHex(u8a))).toBe(true);
});
it('compares against another enum', (): void => {
expect(test.eq(new Enum(registry, DEF, u8a))).toBe(true);
});
it('compares against another object', (): void => {
expect(test.eq({ str: 'XYZ' })).toBe(true);
});
it('compares against values', (): void => {
expect(test.eq('XYZ')).toBe(true);
});
it('compares basic enum on string', (): void => {
expect(new Enum(registry, ['A', 'B', 'C'], 1).eq('B')).toBe(true);
});
});
});
describe('string-only construction (old Enum)', (): void => {
const testDecode = (type: string, input: any, expected: any): void =>
it(`can decode from ${type}`, (): void => {
const e = new Enum(registry, ['foo', 'bar'], input);
expect(e.toString()).toBe(expected);
});
const testEncode = (to: 'toJSON' | 'toNumber' | 'toString' | 'toU8a', expected: any): void =>
it(`can encode ${to}`, (): void => {
const e = new Enum(registry, ['Foo', 'Bar'], 1);
expect(e[to]()).toEqual(expected);
});
testDecode('Enum', undefined, 'foo');
testDecode('Enum', new Enum(registry, ['foo', 'bar'], 1), 'bar');
testDecode('number', 0, 'foo');
testDecode('number', 1, 'bar');
testDecode('string', 'bar', 'bar');
testDecode('Uint8Array', Uint8Array.from([0]), 'foo');
testDecode('Uint8Array', Uint8Array.from([1]), 'bar');
testEncode('toJSON', 'Bar');
testEncode('toNumber', 1);
testEncode('toString', 'Bar');
testEncode('toU8a', Uint8Array.from([1]));
it('provides a clean toString()', (): void => {
expect(
new Enum(registry, ['foo', 'bar']).toString()
).toEqual('foo');
});
it('provides a clean toString() (enum)', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], new Enum(registry, ['foo', 'bar'], 1)).toNumber()
).toEqual(1);
});
it('converts to and from Uint8Array', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], new Uint8Array([1])).toU8a()
).toEqual(new Uint8Array([1]));
});
it('converts from JSON', (): void => {
expect(
new Enum(registry, ['foo', 'bar', 'baz', 'gaz', 'jaz'], 4).toNumber()
).toEqual(4);
});
it('has correct isXyz getters (Enum.with)', (): void => {
const test = new (Enum.with(['First', 'Second', 'Third']))(registry, 'Second') as any as { isFirst: boolean; isSecond: boolean; isThird: boolean };
expect([test.isFirst, test.isSecond, test.isThird]).toEqual([false, true, false]);
});
describe('utils', (): void => {
it('compares against the index value', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], 1).eq(1)
).toBe(true);
});
it('compares against the index value (false)', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], 1).eq(0)
).toBe(false);
});
it('compares against the string value', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], 1).eq('bar')
).toBe(true);
});
it('compares against the string value (false)', (): void => {
expect(
new Enum(registry, ['foo', 'bar'], 1).eq('foo')
).toBe(false);
});
it('has isNone set, with correct index (i.e. no values are used)', (): void => {
const test = new Enum(registry, ['foo', 'bar'], 1);
expect(test.isNone).toBe(true);
expect(test.index).toEqual(1);
});
it('has a sane inspect', (): void => {
expect(
new Enum(
registry,
{ Text, U32 },
'0x0134120000'
).inspect()
).toEqual({
inner: undefined,
outer: [new Uint8Array([0x01]), new Uint8Array([0x34, 0x12, 0x00, 0x00])]
});
});
});
});
describe('index construction', (): void => {
it('creates enum where index is specified', (): void => {
const Test = Enum.with({
A: U32,
B: U32
});
const test = new Test(registry, new U32(registry, 123), 1);
expect(test.type).toEqual('B');
expect((test.value as U32).toNumber()).toEqual(123);
});
it('creates enum when value is an enum', (): void => {
const Test = Enum.with({
A: U32,
B: U32
});
const test = new Test(registry, new Test(registry, 123, 1));
expect(test.type).toEqual('B');
expect((test.value as U32).toNumber()).toEqual(123);
});
it('creates via enum with nested enums as the value', (): void => {
const Nest = Enum.with({
C: U32,
D: U32
});
const Test = Enum.with({
A: U32,
B: Nest
});
const test = new Test(registry, new Nest(registry, 123, 1), 1);
expect(test.type).toEqual('B');
expect((test.value as Enum).type).toEqual('D');
expect(((test.value as Enum).value as U32).toNumber()).toEqual(123);
});
});
describe('toRawType', (): void => {
it('has a sane output for basic enums', (): void => {
expect(
new Enum(registry, ['foo', 'bar']).toRawType()
).toEqual(stringify({ _enum: ['foo', 'bar'] }));
});
it('has a sane output for typed enums', (): void => {
expect(
// eslint-disable-next-line sort-keys
new Enum(registry, { foo: Text, bar: U32 }).toRawType()
// eslint-disable-next-line sort-keys
).toEqual(stringify({ _enum: { foo: 'Text', bar: 'u32' } }));
});
it('re-creates via rawType (c-like)', (): void => {
const type = new Enum(registry, ['foo', 'bar']).toRawType() as 'Raw';
expect(registry.createType(type, 1).toString()).toEqual('bar');
});
it('re-creates via rawType (types)', (): void => {
const type = new Enum(registry, { A: Text, B: U32, C: U32 }).toRawType();
const value = registry.createType(type, { B: 123 });
expect((value as unknown as { isB: boolean }).isB).toEqual(true);
expect((value as unknown as { asB: U32 }).asB.toNumber()).toEqual(123);
});
});
describe('indexed enum', (): void => {
const Test = Enum.with({
A: 5,
B: 42,
C: 69,
D: 255
});
it('handles an indexed C-like enum', (): void => {
expect(new Test(registry, 'A').toNumber()).toEqual(5);
expect(new Test(registry, 'B').toNumber()).toEqual(42);
expect(new Test(registry, 'C').toNumber()).toEqual(69);
expect(new Test(registry, 69).toNumber()).toEqual(69);
expect(new Test(registry, 'D').toNumber()).toEqual(255);
});
it('creates proper raw structure', (): void => {
expect(new Test(registry).toRawType()).toEqual(stringify({
_enum: {
A: 5,
B: 42,
C: 69,
D: 255
}
}));
});
it('has the indexes for the enum', (): void => {
expect(new Test(registry).defIndexes).toEqual([5, 42, 69, 255]);
});
it('has the correct outputs', (): void => {
const test = new Test(registry, 5);
expect(test.toU8a()).toEqual(new Uint8Array([5]));
expect(test.toHex()).toEqual('0x05');
expect(test.toJSON()).toEqual('A');
});
});
describe('toHex', (): void => {
it('has a proper hex representation & length', (): void => {
const Test = Enum.with({
A: Text,
B: U32
});
const test = new Test(registry, 123, 1);
expect(test.toHex()).toEqual('0x017b000000');
expect(test.encodedLength).toEqual(1 + 4);
});
it('encodes a single entry correctly', (): void => {
const Test = Enum.with({ A: 'u32' });
const test = new Test(registry, 0x44332211, 0);
expect(test.toHex()).toEqual(
'0x' +
'00' + // index
'11223344' // u32 LE encoded
);
});
it('encodes a single entry correctly (with embedded encoding)', (): void => {
const Test = Enum.with({ A: 'MultiAddress' });
const test = new Test(registry, registry.createType('AccountId', '0x0001020304050607080910111213141516171819202122232425262728293031'), 0);
expect(test.toHex()).toEqual(
'0x' +
'00' + // index
'00' + // MultiAddress indicating an embedded AccountId
'0001020304050607080910111213141516171819202122232425262728293031' // AccountId
);
});
});
describe('toU8a', (): void => {
const Test = Enum.with({
A: Text,
B: U32
});
it('has a correct toU8a() output', (): void => {
expect(
new Test(registry, { B: 69 }).toU8a()
).toEqual(
new Uint8Array([1, 69, 0, 0, 0])
);
});
it('has a correct toU8a(true) output', (): void => {
expect(
new Test(registry, { B: 69 }).toU8a(true)
).toEqual(
new Uint8Array([69, 0, 0, 0])
);
});
});
perf('Enum', 20_000, [[new Uint8Array([0, 31, 32, 33, 34])]], (v: Uint8Array) => new PEnum(registry, v));
});
+460
View File
@@ -0,0 +1,460 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, CodecClass, DefinitionSetter, IEnum, Inspect, IU8a, Registry } from '../types/index.js';
import { identity, isHex, isNumber, isObject, isString, isU8a, objectProperties, stringCamelCase, stringify, stringPascalCase, u8aConcatStrict, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { mapToTypeMap, typesToMap } from '../utils/index.js';
import { Null } from './Null.js';
// export interface, this is used in Enum.with, so required as public by TS
export type EnumCodecClass<T = Codec> = new(registry: Registry, value?: any, index?: number) => T;
interface Definition {
def: TypesDef;
isBasic: boolean;
isIndexed: boolean;
}
interface EntryDef {
Type: CodecClass;
index: number;
}
type TypesDef = Record<string, EntryDef>;
interface Decoded {
index: number;
value: Codec;
}
function isRustEnum (def: Record<string, string | CodecClass> | Record<string, number>): def is Record<string, string | CodecClass> {
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: Registry, _def: Record<string, string | CodecClass> | Record<string, number> | string[]): Definition {
const def: TypesDef = {};
let isBasic: boolean;
let isIndexed: boolean;
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: TypesDef, checkIdx: number): CodecClass {
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: Registry, def: TypesDef, index: number, value: Uint8Array): Decoded {
const Type = getEntryType(def, index);
return {
index,
value: new Type(registry, value)
};
}
function createFromValue (registry: Registry, def: TypesDef, index = 0, value?: unknown): Decoded {
const Type = getEntryType(def, index);
return {
index,
value: value instanceof Type
? value
: new Type(registry, value)
};
}
function decodeFromJSON (registry: Registry, def: TypesDef, key: string, value?: unknown): Decoded {
// 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 as Error).message}`);
}
}
function decodeEnum (registry: Registry, def: TypesDef, value?: unknown, index?: number): Decoded {
// 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 implements IEnum {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #def: TypesDef;
readonly #entryIndex: number;
readonly #indexes: number[];
readonly #isBasic: boolean;
readonly #isIndexed: boolean;
readonly #raw: Codec;
constructor (registry: Registry, Types: Record<string, string | CodecClass> | Record<string, number> | string[], value?: unknown, index?: number, { definition, setDefinition = identity }: DefinitionSetter<Definition> = {}) {
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.#def = def;
this.#isBasic = isBasic;
this.#isIndexed = isIndexed;
this.#indexes = Object.values(def).map(({ index }) => index);
this.#entryIndex = this.#indexes.indexOf(decoded.index);
this.#raw = decoded.value;
if (this.#raw.initialU8aLength) {
this.initialU8aLength = 1 + this.#raw.initialU8aLength;
}
}
public static with (Types: Record<string, string | CodecClass> | Record<string, number> | string[]): EnumCodecClass<Enum> {
let definition: Definition | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = (d: Definition) =>
definition = d;
return class extends Enum {
static {
const keys = Array.isArray(Types)
? Types
: Object.keys(Types);
const count = keys.length;
const asKeys = new Array<string>(count);
const isKeys = new Array<string>(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, (_: string, i: number, self: Enum) =>
self.type === keys[i]
);
objectProperties(this.prototype, asKeys, (k: string, i: number, self: Enum): Codec => {
if (self.type !== keys[i]) {
throw new Error(`Cannot convert '${self.type}' via ${k}`);
}
return self.value;
});
}
constructor (registry: Registry, value?: unknown, index?: number) {
super(registry, Types, value, index, { definition, setDefinition });
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return 1 + this.#raw.encodedLength;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description The index of the enum value
*/
public get index (): number {
return this.#indexes[this.#entryIndex];
}
/**
* @description The value of the enum
*/
public get inner (): Codec {
return this.#raw;
}
/**
* @description true if this is a basic enum (no values)
*/
public get isBasic (): boolean {
return this.#isBasic;
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.#raw.isEmpty;
}
/**
* @description Checks if the Enum points to a [[Null]] type
*/
public get isNone (): boolean {
return this.#raw instanceof Null;
}
/**
* @description The available keys for this enum
*/
public get defIndexes (): number[] {
return this.#indexes;
}
/**
* @description The available keys for this enum
*/
public get defKeys (): string[] {
return Object.keys(this.#def);
}
/**
* @description The name of the type this enum value represents
*/
public get type (): string {
return this.defKeys[this.#entryIndex];
}
/**
* @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?: unknown): boolean {
// 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.#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
*/
public inspect (): Inspect {
if (this.#isBasic) {
return { outer: [new Uint8Array([this.index])] };
}
const { inner, outer = [] } = this.#raw.inspect();
return {
inner,
outer: [new Uint8Array([this.index]), ...outer]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
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, disableAscii?: boolean): AnyJson {
return this.#isBasic || this.isNone
? this.type
: { [this.type]: this.#raw.toHuman(isExtended, disableAscii) };
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this.#isBasic
? this.type
: { [stringCamelCase(this.type)]: this.#raw.toJSON() };
}
/**
* @description Returns the number representation for the value
*/
public toNumber (): number {
return this.index;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
return this.#isBasic
? this.type
: { [stringCamelCase(this.type)]: this.#raw.toPrimitive(disableAscii) };
}
/**
* @description Returns a raw struct representation of the enum types
*/
protected _toRawStruct (): string[] | Record<string, string | number> {
if (this.#isBasic) {
return this.#isIndexed
? this.defKeys.reduce((out: Record<string, number>, key, index): Record<string, number> => {
out[key] = this.#indexes[index];
return out;
}, {})
: this.defKeys;
}
const entries = Object.entries(this.#def);
return typesToMap(this.registry, entries.reduce<[CodecClass[], string[]]>((out, [key, { Type }], i) => {
out[0][i] = Type;
out[1][i] = key;
return out;
}, [new Array<CodecClass>(entries.length), new Array<string>(entries.length)]));
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return stringify({ _enum: this._toRawStruct() });
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
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)
*/
public toU8a (isBare?: boolean): Uint8Array {
return isBare
? this.#raw.toU8a(isBare)
: u8aConcatStrict([
new Uint8Array([this.index]),
this.#raw.toU8a(isBare)
]);
}
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { UIntBitLength } from '../types/index.js';
import { TypeRegistry } from '@pezkuwi/types';
import { Int } from '@pezkuwi/types-codec';
import { BN } from '@pezkuwi/util';
const TESTS: [bitLength: UIntBitLength, value: string | number | Uint8Array, expected?: string][] = [
[8, 0],
[8, 127],
[8, -128],
[8, new Uint8Array([0])],
[8, new Uint8Array([127])],
[8, new Uint8Array([128]), '-128'],
[32, 0],
[32, 2147483647],
[32, -2147483648]
];
describe('Int', (): void => {
const registry = new TypeRegistry();
it('can construct via a single-entry struct', (): void => {
expect(
// @ts-expect-error We could receive these via JSON
new Int(registry, { ref_time: 1234 }).toNumber()
).toEqual(1234);
expect(
// @ts-expect-error We could receive these via JSON
() => new Int(registry, { ref_time: 1234, zoo: 4567 }).toNumber()
).toThrow(/Unable to construct number from/);
});
it('converts to Little Endian from the provided value', (): void => {
expect(
new Int(registry, -1234).toU8a()
).toEqual(new Uint8Array([46, 251, 255, 255, 255, 255, 255, 255]));
});
it('decodes edge case to bytes correctly', (): void => {
// Zero
expect(
new Int(registry, 0, 8).toU8a()
).toEqual(new Uint8Array([0]));
expect(
new Int(registry, 0, 16).toU8a()
).toEqual(new Uint8Array([0, 0]));
expect(
new Int(registry, 0, 32).toU8a()
).toEqual(new Uint8Array([0, 0, 0, 0]));
expect(
new Int(registry, 0, 64).toU8a()
).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]));
expect(
new Int(registry, 0, 128).toU8a()
).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
// One
expect(
new Int(registry, 0, 8).toU8a()
).toEqual(new Uint8Array([0]));
expect(
new Int(registry, 1, 16).toU8a()
).toEqual(new Uint8Array([1, 0]));
expect(
new Int(registry, 1, 32).toU8a()
).toEqual(new Uint8Array([1, 0, 0, 0]));
expect(
new Int(registry, 1, 64).toU8a()
).toEqual(new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0]));
expect(
new Int(registry, 1, 128).toU8a()
).toEqual(new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
// MIN
expect(
new Int(registry, -128, 8).toU8a()
).toEqual(new Uint8Array([128]));
expect(
new Int(registry, -32768, 16).toU8a()
).toEqual(new Uint8Array([0, 128]));
// MAX
expect(
new Int(registry, 127, 8).toU8a()
).toEqual(new Uint8Array([127]));
expect(
new Int(registry, 32767, 16).toU8a()
).toEqual(new Uint8Array([255, 127]));
});
it('decodes edge case to js number', (): void => {
// Zero
expect(
new Int(registry, new Uint8Array([0]), 8).toNumber()
).toEqual(0);
expect(
new Int(registry, new Uint8Array([0, 0]), 16).toNumber()
).toEqual(0);
// One
expect(
new Int(registry, new Uint8Array([1]), 8).toNumber()
).toEqual(1);
expect(
new Int(registry, new Uint8Array([1, 0]), 16).toNumber()
).toEqual(1);
// MIN
expect(
new Int(registry, new Uint8Array([128]), 8).toNumber()
).toEqual(-128);
expect(
new Int(registry, new Uint8Array([128, 255]), 16).toNumber()
).toEqual(-128);
// MAX
expect(
new Int(registry, new Uint8Array([127]), 8).toNumber()
).toEqual(127);
expect(
new Int(registry, new Uint8Array([255, 127]), 16).toNumber()
).toEqual(32767);
});
it('converts to Little Endian from the provided value (bitLength)', (): void => {
expect(
new Int(registry, -1234, 32).toU8a()
).toEqual(new Uint8Array([46, 251, 255, 255]));
});
it('converts to equivalents', (): void => {
const a = new Int(registry, '-123');
expect(
new Int(registry, a).toNumber()
).toEqual(-123);
});
it('allows null/undefined', (): void => {
expect(
new Int(registry).toNumber()
).toEqual(0);
expect(
new Int(registry, null).toNumber()
).toEqual(0);
});
describe('utilities', (): void => {
it('provides a toBigInt interface', (): void => {
expect(
new Int(registry, -1234).toBigInt()
).toEqual(-1234n);
});
it('provides a toBn interface', (): void => {
expect(
new Int(registry, -1234).toBn().toNumber()
).toEqual(-1234);
});
it('provides a toNumber interface', (): void => {
expect(
new Int(registry, -1234).toNumber()
).toEqual(-1234);
});
it('has a sane inspect', (): void => {
expect(
new Int(registry, '0x12', 16).inspect()
).toEqual({
outer: [new Uint8Array([0x12, 0x00])]
});
});
it('converts to hex/string', (): void => {
const i = new Int(registry, '0x12', 16);
expect(i.toHex()).toEqual('0x0012');
expect(i.toString()).toEqual('18');
});
});
describe('static with', (): void => {
it('allows default toRawType', (): void => {
expect(
new (Int.with(64))(registry).toRawType()
).toEqual('i64');
});
it('allows toRawType override', (): void => {
expect(
new (Int.with(64, 'SomethingElse'))(registry).toRawType()
).toEqual('SomethingElse');
});
});
describe('conversion tests', (): void => {
TESTS.forEach(([bitLength, input, expected], i): void => {
it(`#${i}: converts ${input as string}`, (): void => {
expect(
new Int(registry, Array.isArray(input) ? new Uint8Array(input) : input, bitLength).toString()
).toEqual(expected || new BN(input).toString());
});
});
});
});
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyNumber, CodecClass, Registry, UIntBitLength } from '../types/index.js';
import { AbstractInt } from '../abstract/Int.js';
/**
* @name Int
* @description
* A generic signed integer codec. For Bizinikiwi all numbers are Little Endian encoded,
* this handles the encoding and decoding of those numbers. Upon construction
* the bitLength is provided and any additional use keeps the number to this
* length. This extends `BN`, so all methods available on a normal `BN` object
* is available here.
* @noInheritDoc
*/
export class Int extends AbstractInt {
constructor (registry: Registry, value: AnyNumber | null = 0, bitLength?: UIntBitLength) {
super(registry, value, bitLength, true);
}
public static with (bitLength: UIntBitLength, typeName?: string): CodecClass<Int> {
return class extends Int {
constructor (registry: Registry, value?: AnyNumber | null) {
super(registry, value, bitLength);
}
public override toRawType (): string {
return typeName || super.toRawType();
}
};
}
}
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Null } from '@pezkuwi/types-codec';
describe('Null', (): void => {
const registry = new TypeRegistry();
it('compares against null', (): void => {
expect(new Null(registry).eq(null)).toBe(true);
});
it('compares against Null', (): void => {
expect(new Null(registry).eq(new Null(registry))).toBe(true);
});
it('compares against other (failed)', (): void => {
expect(new Null(registry).eq()).toBe(false);
});
it('has no hash', (): void => {
expect(
() => new Null(registry).hash
).toThrow();
});
it('isEmpty', (): void => {
expect(new Null(registry).isEmpty).toBe(true);
});
it('has an empty hex', (): void => {
expect(new Null(registry).toHex()).toEqual('0x');
});
it('has a Null type', (): void => {
expect(new Null(registry).toRawType()).toEqual('Null');
});
});
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { Codec, Inspect, IU8a, Registry } from '../types/index.js';
import { isNull } from '@pezkuwi/util';
/**
* @name Null
* @description
* Implements a type that does not contain anything (apart from `null`)
*/
export class Null implements Codec {
readonly encodedLength = 0;
readonly isEmpty = true;
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength = 0;
public isStorageFallback?: boolean;
constructor (registry: Registry) {
this.registry = registry;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
throw new Error('.hash is not implemented on Null');
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return other instanceof Null || isNull(other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
return '0x';
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): null {
return this.toJSON();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): null {
return null;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): null {
return null;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'Null';
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return '';
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return new Uint8Array();
}
}
@@ -0,0 +1,216 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { bool, Bytes, Null, Option, Text, U32 } from '@pezkuwi/types-codec';
const registry = new TypeRegistry();
const testDecode = (type: string, input: any, expected: string): void =>
it(`can decode from ${type}`, (): void => {
const o = new Option(registry, Text, input);
expect(o.toString()).toBe(expected);
expect(o.isNone).toBe(!expected.length);
});
const testEncode = (to: 'toHex' | 'toString' | 'toU8a', expected: any): void =>
it(`can encode ${to}`, (): void => {
const e = new Option(registry, Text, 'foo');
expect(e[to]()).toEqual(expected);
});
describe('Option', (): void => {
it('converts undefined/null to empty', (): void => {
expect(new Option(registry, Text, undefined).isNone).toBe(true);
expect(new Option(registry, Text, null).isNone).toBe(true);
expect(new Option(registry, Text, 'test').isNone).toBe(false);
expect(new Option(registry, Text, '0x').isNone).toBe(true);
expect(new Option(registry, '()', null).isNone).toBe(true);
});
it('can wrap an Option<Null>/Option<()>', (): void => {
[
new Option(registry, Null, new Null(registry)),
new Option(registry, '()', new Null(registry))
].forEach((test): void => {
expect(test.isSome).toBe(true);
expect(test.isNone).toBe(false);
expect(test.isEmpty).toBe(false);
expect(test.toU8a()).toEqual(new Uint8Array([1]));
expect(test.unwrap().toHex()).toEqual('0x');
});
});
it('can decode a nested Option', (): void => {
expect(
new Option(
registry,
Option.with(Option.with(Text)),
new Option(
registry,
Option.with(Text),
new Option(
registry,
Text,
new Uint8Array([1, 3 << 2, 66, 67, 68])
)
)
).toU8a()
).toEqual(new Uint8Array([1, 1, 1, 3 << 2, 66, 67, 68]));
});
it('can convert between different Some/None', (): void => {
const def = '{ "foo":"Text", "zar":"Text" }';
const none = new Option(registry, def, null);
const some = new Option(registry, def, new Option(registry, def, { foo: 'a', zar: 'b' }));
expect(new Option(registry, def, none).isNone).toBe(true);
expect(new Option(registry, def, some).isNone).toBe(false);
expect(new Option(registry, def, some).unwrap().toHuman()).toEqual({ foo: 'a', zar: 'b' });
});
it('correctly handles booleans', (): void => {
expect(new Option(registry, bool).isNone).toBe(true);
expect(new Option(registry, bool, true).isSome).toBe(true);
expect(new Option(registry, bool, true).unwrap().isTrue).toBe(true);
expect(new Option(registry, bool, false).isSome).toBe(true);
expect(new Option(registry, bool, false).unwrap().isTrue).toBe(false);
});
it('converts an option to an option', (): void => {
expect(
new Option(registry, Text, new Option(registry, Text, 'hello')).toString()
).toEqual('hello');
});
it('converts an option to an option (strings)', (): void => {
expect(
new Option(registry, 'Text', new Option(registry, 'Text', 'hello')).toString()
).toEqual('hello');
});
it('converts correctly from hex with toHex (Bytes)', (): void => {
// Option<Bytes> for a teyrchain head, however, this is effectively an
// Option<Option<Bytes>> (hence the length, since it is from storage)
const HEX = '0x210100000000000000000000000000000000000000000000000000000000000000000000000000000000011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce';
// watch the hex prefix and length
expect(
new Option(registry, Bytes, HEX).toHex().substring(6)
).toEqual(HEX.substring(2));
});
it('converts correctly from hex with toNumber (U64)', (): void => {
const HEX = '0x12345678';
expect(
new Option(registry, U32, HEX).unwrap().toNumber()
).toEqual(0x12345678);
});
it('decodes reusing instanciated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new Option(registry, Text, foo)).value
).toBe(foo);
});
testDecode('string (with)', 'foo', 'foo');
testDecode('string (without)', undefined, '');
testDecode('Uint8Array (with)', Uint8Array.from([1, 12, 102, 111, 111]), 'foo');
testDecode('Uint8Array (without)', Uint8Array.from([0]), '');
testEncode('toHex', '0x0c666f6f');
testEncode('toString', 'foo');
testEncode('toU8a', Uint8Array.from([1, 12, 102, 111, 111]));
it('has empty toString() (undefined)', (): void => {
expect(
new Option(registry, Text).toString()
).toEqual('');
});
it('has value toString() (provided)', (): void => {
expect(
new Option(registry, Text, new Uint8Array([1, 4 << 2, 49, 50, 51, 52])).toString()
).toEqual('1234');
});
it('converts toU8a() with', (): void => {
expect(
new Option(registry, Text, '1234').toU8a()
).toEqual(new Uint8Array([1, 4 << 2, 49, 50, 51, 52]));
});
it('allows bare specifiers on toU8a', (): void => {
expect(
new Option(registry, Text, '1234').toU8a(true)
).toEqual(new Uint8Array([49, 50, 51, 52]));
});
it('converts toU8a() without', (): void => {
expect(
new Option(registry, Text).toU8a()
).toEqual(new Uint8Array([0]));
});
it('converts toJSON() as null without', (): void => {
expect(
new Option(registry, Text).toJSON()
).toEqual(null);
});
it('converts toJSON() as non-null with Bytes', (): void => {
expect(
new Option(registry, Bytes, 'abcde').toJSON()
).toEqual('0x6162636465');
});
it('converts toJSON() as non-null with Text', (): void => {
expect(
new Option(registry, Text, 'abcde').toJSON()
).toEqual('abcde');
});
describe('utils', (): void => {
const test = new Option(registry, Text, '1234');
it('compares against other option', (): void => {
expect(test.eq(new Option(registry, Text, '1234'))).toBe(true);
});
it('compares against raw value', (): void => {
expect(test.eq('1234')).toBe(true);
});
it('unwrapOr to specified if empty', (): void => {
expect(new Option(registry, Text).unwrapOr('6789').toString()).toEqual('6789');
});
it('unwrapOr to specified if non-empty', (): void => {
expect(new Option(registry, Text, '1234').unwrapOr(null)?.toString()).toEqual('1234');
});
it('unwrapOrDefault to default if empty', (): void => {
expect(new Option(registry, U32).unwrapOrDefault().toNumber()).toEqual(0);
});
it('unwrapOrDefault to specified if non-empty', (): void => {
expect(new Option(registry, U32, '1234').unwrapOrDefault().toNumber()).toEqual(1234);
});
it('has a sane inspect', (): void => {
expect(
new Option(registry, U32, '1234').inspect()
).toEqual({
inner: undefined,
outer: [new Uint8Array([0x01]), new Uint8Array([210, 4, 0, 0])]
});
});
});
});
+275
View File
@@ -0,0 +1,275 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, CodecClass, DefinitionSetter, Inspect, IOption, IU8a, Registry } from '../types/index.js';
import { identity, isCodec, isNull, isU8a, isUndefined, u8aToHex } from '@pezkuwi/util';
import { typeToConstructor } from '../utils/index.js';
import { Null } from './Null.js';
class None extends Null {
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return 'None';
}
}
/** @internal */
function decodeOption (registry: Registry, Type: CodecClass, value?: unknown): Codec {
if (value instanceof Type) {
// don't re-create, use as it (which also caters for derived types)
return value;
} else if (value instanceof Option) {
if (value.value instanceof Type) {
// same instance, return it
return value.value;
} else if (value.isNone) {
// internal is None, we are also none
return new None(registry);
}
// convert the actual value into known
return new Type(registry, value.value);
} else if (isNull(value) || isUndefined(value) || value === '0x' || value instanceof None) {
// anything empty we pass as-is
return new None(registry);
} 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 None(registry)
: new Type(registry, value.subarray(1));
}
return new Type(registry, value);
}
/**
* @name Option
* @description
* An Option is an optional field. Basically the first byte indicates that there is
* is value to follow. If the byte is `1` there is an actual value. So the Option
* implements that - decodes, checks for optionality and wraps the required structure
* with a value if/as required/found.
*/
export class Option<T extends Codec> implements IOption<T> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #Type: CodecClass<T>;
readonly #raw: T;
constructor (registry: Registry, typeName: CodecClass<T> | string, value?: unknown, { definition, setDefinition = identity }: DefinitionSetter<CodecClass<T>> = {}) {
const Type = definition || setDefinition(typeToConstructor(registry, typeName));
const decoded = isU8a(value) && value.length && !isCodec(value)
? value[0] === 0
? new None(registry)
: new Type(registry, value.subarray(1))
: decodeOption(registry, Type, value);
this.registry = registry;
this.#Type = Type;
this.#raw = decoded as T;
if (decoded?.initialU8aLength) {
this.initialU8aLength = 1 + decoded.initialU8aLength;
}
}
public static with<O extends Codec> (Type: CodecClass<O> | string): CodecClass<Option<O>> {
let definition: CodecClass<O> | undefined;
const setDefinition = <T> (d: CodecClass<T>): CodecClass<T> => {
definition = d as unknown as CodecClass<O>;
return d;
};
return class extends Option<O> {
constructor (registry: Registry, value?: unknown) {
super(registry, Type, value, { definition, setDefinition });
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
// boolean byte (has value, doesn't have) along with wrapped length
return 1 + this.#raw.encodedLength;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the Option has no value
*/
public get isEmpty (): boolean {
return this.isNone;
}
/**
* @description Checks if the Option has no value
*/
public get isNone (): boolean {
return this.#raw instanceof None;
}
/**
* @description Checks if the Option has a value
*/
public get isSome (): boolean {
return !this.isNone;
}
/**
* @description The actual value for the Option
*/
public get value (): T {
return this.#raw;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
if (other instanceof Option) {
return (this.isSome === other.isSome) && this.value.eq(other.value);
}
return this.value.eq(other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
if (this.isNone) {
return { outer: [new Uint8Array([0])] };
}
const { inner, outer = [] } = this.#raw.inspect();
return {
inner,
outer: [new Uint8Array([1]), ...outer]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
// This attempts to align with the JSON encoding - actually in this case
// the isSome value is correct, however the `isNone` may be problematic
return this.isNone
? '0x'
: u8aToHex(this.toU8a().subarray(1));
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson {
return this.#raw.toHuman(isExtended, disableAscii);
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this.isNone
? null
: this.#raw.toJSON();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
return this.isNone
? null
: this.#raw.toPrimitive(disableAscii);
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (isBare?: boolean): string {
const wrapped = this.registry.getClassName(this.#Type) || new this.#Type(this.registry).toRawType();
return isBare
? wrapped
: `Option<${wrapped}>`;
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.#raw.toString();
}
/**
* @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 {
if (isBare) {
return this.#raw.toU8a(true);
}
const u8a = new Uint8Array(this.encodedLength);
if (this.isSome) {
u8a.set([1]);
u8a.set(this.#raw.toU8a(), 1);
}
return u8a;
}
/**
* @description Returns the value that the Option represents (if available), throws if null
*/
public unwrap (): T {
if (this.isNone) {
throw new Error('Option: unwrapping a None value');
}
return this.#raw;
}
/**
* @description Returns the value that the Option represents (if available) or defaultValue if none
* @param defaultValue The value to return if the option isNone
*/
public unwrapOr<O> (defaultValue: O): T | O {
return this.isSome
? this.unwrap()
: defaultValue;
}
/**
* @description Returns the value that the Option represents (if available) or defaultValue if none
* @param defaultValue The value to return if the option isNone
*/
public unwrapOrDefault (): T {
return this.isSome
? this.unwrap()
: new this.#Type(this.registry);
}
}
@@ -0,0 +1,64 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Result, Text, u32 } from '@pezkuwi/types-codec';
import { hexToString } from '@pezkuwi/util';
describe('Result', (): void => {
const registry = new TypeRegistry();
const Type = Result.with({ Err: Text, Ok: u32 });
it('has a sane toRawType representation', (): void => {
expect(new Type(registry).toRawType()).toEqual('Result<u32,Text>');
});
it('decodes from a u8a (success)', (): void => {
const result = new Type(registry, new Uint8Array([0, 1, 2, 3, 4]));
expect(result.isOk).toBe(true);
expect(result.asOk.toU8a()).toEqual(new Uint8Array([1, 2, 3, 4]));
expect(result.toHex()).toEqual('0x0001020304');
expect(result.toJSON()).toEqual({
ok: 0x04030201
});
});
it('decodes from a u8a (error)', (): void => {
const result = new Type(registry, new Uint8Array([1, 4 << 2, 100, 101, 102, 103]));
expect(result.isErr).toBe(true);
expect(result.asErr.toU8a()).toEqual(new Uint8Array([4 << 2, 100, 101, 102, 103]));
expect(result.toHex()).toEqual('0x011064656667');
expect(result.toJSON()).toEqual({
err: hexToString('0x64656667')
});
});
it('decodes from a JSON representation', (): void => {
const result = new Type(registry, { Err: 'error' });
expect(result.isErr).toBe(true);
expect(result.asErr.toString()).toEqual('error');
expect(result.toHex()).toEqual('0x01146572726f72');
});
it('decodes reusing instanciated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
new Result(
registry,
Text,
Text,
{ Ok: foo }
).asOk
).toBe(foo);
});
it('returns a proper raw typedef rom a built-in', (): void => {
expect(registry.createType('DispatchResult').toRawType()).toEqual('Result<(),DispatchError>');
});
});
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec, CodecClass, IResult, Registry } from '../types/index.js';
import { Enum } from './Enum.js';
/**
* @name Result
* @description
* A Result maps to the Rust Result type, that can either wrap a success or error value
*/
export class Result<O extends Codec, E extends Codec> extends Enum implements IResult<O, E> {
constructor (registry: Registry, Ok: CodecClass<O> | string, Err: CodecClass<E> | string, value?: unknown) {
// NOTE This is order-dependent, Ok (with index 0) needs to be first
// eslint-disable-next-line sort-keys
super(registry, { Ok, Err }, value);
}
public static override with<O extends Codec, E extends Codec> (Types: { Ok: CodecClass<O> | string; Err: CodecClass<E> | string }): CodecClass<Result<O, E>> {
return class extends Result<O, E> {
constructor (registry: Registry, value?: unknown) {
super(registry, Types.Ok, Types.Err, value);
}
};
}
/**
* @description Returns the wrapper Err value (if isErr)
*/
public get asErr (): E {
if (!this.isErr) {
throw new Error('Cannot extract Err value from Ok result, check isErr first');
}
return this.value as E;
}
/**
* @description Returns the wrapper Ok value (if isOk)
*/
public get asOk (): O {
if (!this.isOk) {
throw new Error('Cannot extract Ok value from Err result, check isOk first');
}
return this.value as O;
}
/**
* @description Checks if the Result has no value
*/
public override get isEmpty (): boolean {
return this.isOk && this.value.isEmpty;
}
/**
* @description Checks if the Result wraps an Err value
*/
public get isErr (): boolean {
return !this.isOk;
}
/**
* @description Checks if the Result wraps an Ok value
*/
public get isOk (): boolean {
return this.index === 0;
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
const Types = this._toRawStruct() as { Ok: unknown; Err: unknown };
return `Result<${Types.Ok as string},${Types.Err as string}>`;
}
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { BlockNumber, VoteThreshold } from '@pezkuwi/types/interfaces';
import type { AnyTupleValue, CodecTo } from '@pezkuwi/types-codec/types';
import { Metadata, TypeRegistry } from '@pezkuwi/types';
import { Text, Tuple, U32, U128 } from '@pezkuwi/types-codec';
import rpcMetadata from '@pezkuwi/types-support/metadata/static-bizinikiwi';
import { stringToU8a } from '@pezkuwi/util';
describe('Tuple', (): void => {
const registry = new TypeRegistry();
let tuple: Tuple;
beforeEach((): void => {
tuple = new Tuple(
registry,
[Text, U32],
['bazzing', 69]
);
});
describe('constructor', (): void => {
it('fails construction on non-Array, non-Hex inputs', (): void => {
// @ts-expect-error We are intentionally passing a non-valid input
expect(() => new Tuple(registry, [Text, Text], '12345')).toThrow(/Expected array input to Tuple decoding, found string/);
// @ts-expect-error We are intentionally passing a non-valid input
expect(() => new Tuple(registry, [Text, Text], {})).toThrow(/Expected array input to Tuple decoding, found object/);
});
});
describe('decoding', (): void => {
const testDecode = (type: string, input: AnyTupleValue): void =>
it(`can decode from ${type}`, (): void => {
const t = new Tuple(registry, [
Text,
U32
], input);
expect(
t.toJSON()
).toEqual(['bazzing', 69]);
});
testDecode('array', ['bazzing', 69]);
testDecode('hex', '0x1c62617a7a696e6745000000');
testDecode('Uint8Array', Uint8Array.from([28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]));
it('decodes reusing instantiated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new Tuple(registry, [Text], [foo]))[0]
).toBe(foo);
});
it('decodes properly from complex types', (): void => {
const INPUT = '0xcc0200000000';
const test = registry.createType('(u32, [u32; 0], u16)', INPUT);
expect(test.encodedLength).toEqual(4 + 0 + 2);
expect(test.toHex()).toEqual(INPUT);
});
});
describe('encoding', (): void => {
const testEncode = (to: CodecTo | 'toArray', expected: any): void =>
it(`can encode ${to}`, (): void => {
expect(tuple[to]()).toEqual(expected);
});
testEncode('toHex', '0x1c62617a7a696e6745000000');
testEncode('toJSON', ['bazzing', 69]);
testEncode('toU8a', Uint8Array.from([28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]));
testEncode('toString', '["bazzing",69]');
});
it('creates from string types', (): void => {
expect(
new Tuple(
registry,
['Text', 'u32', U32],
['foo', 69, 42]
).toString()
).toEqual('["foo",69,42]');
});
it('creates properly via actual hex string', (): void => {
const metadata = new Metadata(registry, rpcMetadata);
registry.setMetadata(metadata);
const test = new (Tuple.with([
registry.createClass('BlockNumber'), registry.createClass('VoteThreshold')
]
))(registry, '0x6219000001');
expect((test[0] as BlockNumber).toNumber()).toEqual(6498);
expect((test[1] as VoteThreshold).toNumber()).toEqual(1);
});
it('exposes the Types', (): void => {
expect(tuple.Types).toEqual(['Text', 'u32']);
});
it('exposes the Types (object creation)', (): void => {
const test = new Tuple(registry, {
BlockNumber: registry.createClass('BlockNumber'),
VoteThreshold: registry.createClass('VoteThreshold')
}, []);
expect(test.Types).toEqual(['BlockNumber', 'VoteThreshold']);
});
it('exposes filter', (): void => {
expect(tuple.filter((v): boolean => v.toJSON() === 69)).toEqual([new U32(registry, 69)]);
});
it('exposes map', (): void => {
expect(tuple.map((v): string => v.toString())).toEqual(['bazzing', '69']);
});
describe('utils', (): void => {
it('compares against inputs', (): void => {
expect(tuple.eq(['bazzing', 69])).toBe(true);
});
it('compares against inputs (mismatch)', (): void => {
expect(tuple.eq(['bazzing', 72])).toBe(false);
});
it('has a sane inspect', (): void => {
expect(
tuple.inspect()
).toEqual({
inner: [
{ outer: [new Uint8Array([7 << 2]), stringToU8a('bazzing')] },
{ outer: [new Uint8Array([69, 0, 0, 0])] }
]
});
});
});
describe('toRawType', (): void => {
it('generates sane value with array types', (): void => {
expect(
new Tuple(registry, [U128, registry.createClass('BlockNumber')]).toRawType()
).toEqual('(u128,BlockNumber)');
});
it('generates sane value with object types', (): void => {
expect(
// eslint-disable-next-line sort-keys
new Tuple(registry, { number: U128, blockNumber: registry.createClass('BlockNumber') }).toRawType()
).toEqual('(u128,BlockNumber)');
});
});
});
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyTupleValue, Codec, CodecClass, DefinitionSetter, Inspect, ITuple, Registry } from '../types/index.js';
import { identity, isFunction, isHex, isString, isU8a, stringify, u8aConcatStrict, u8aToU8a } from '@pezkuwi/util';
import { AbstractArray } from '../abstract/Array.js';
import { decodeU8a, mapToTypeMap, typesToConstructors, typeToConstructor } from '../utils/index.js';
type TupleType = (CodecClass | string);
type TupleTypes = TupleType[] | Record<string, CodecClass | string>;
type Definition = [CodecClass[], string[]];
/** @internal */
function decodeTuple (registry: Registry, result: Codec[], value: Exclude<AnyTupleValue, Uint8Array> | undefined, Classes: Definition): [Codec[], number] {
if (Array.isArray(value)) {
const Types = Classes[0];
for (let i = 0, count = Types.length; i < count; i++) {
try {
const entry = value?.[i];
result[i] = entry instanceof Types[i]
? entry
: new Types[i](registry, entry);
} catch (error) {
throw new Error(`Tuple: failed on ${i}:: ${(error as Error).message}`);
}
}
return [result, 0];
} else if (isHex(value)) {
return decodeU8a(registry, result, u8aToU8a(value), Classes);
} else if (!value || !result.length) {
const Types = Classes[0];
for (let i = 0, count = Types.length; i < count; i++) {
result[i] = new Types[i](registry);
}
return [result, 0];
}
throw new Error(`Expected array input to Tuple decoding, found ${typeof value}: ${stringify(value)}`);
}
/**
* @name Tuple
* @description
* A Tuple defines an anonymous fixed-length array, where each element has its
* own type. It extends the base JS `Array` object.
*/
export class Tuple extends AbstractArray<Codec> implements ITuple<Codec[]> {
#Types: Definition;
constructor (registry: Registry, Types: TupleTypes | TupleType, value?: AnyTupleValue, { definition, setDefinition = identity }: DefinitionSetter<Definition> = {}) {
const Classes = definition || setDefinition(
Array.isArray(Types)
? [typesToConstructors(registry, Types), []]
: isFunction(Types) || isString(Types)
? [[typeToConstructor(registry, Types)], []]
: mapToTypeMap(registry, Types)
);
super(registry, Classes[0].length);
this.initialU8aLength = (
isU8a(value)
? decodeU8a(registry, this, value, Classes)
: decodeTuple(registry, this, value, Classes)
)[1];
this.#Types = Classes;
}
public static with (Types: TupleTypes | TupleType): CodecClass<Tuple> {
let definition: Definition | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = (d: Definition) =>
definition = d;
return class extends Tuple {
constructor (registry: Registry, value?: AnyTupleValue) {
super(registry, Types, value, { definition, setDefinition });
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public override get encodedLength (): number {
let total = 0;
for (let i = 0, count = this.length; i < count; i++) {
total += this[i].encodedLength;
}
return total;
}
/**
* @description The types definition of the tuple
*/
public get Types (): string[] {
return this.#Types[1].length
? this.#Types[1]
: this.#Types[0].map((T) => new T(this.registry).toRawType());
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (): Inspect {
return {
inner: this.inspectInner()
};
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
const types = this.#Types[0].map((T) =>
this.registry.getClassName(T) || new T(this.registry).toRawType()
);
return `(${types.join(',')})`;
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
// Overwrite the default toString representation of Array.
return 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 override toU8a (isBare?: boolean): Uint8Array {
return u8aConcatStrict(this.toU8aInner(isBare));
}
}
+192
View File
@@ -0,0 +1,192 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { UInt } from '@pezkuwi/types-codec';
import { BN, BN_TWO, isBn } from '@pezkuwi/util';
import { perf } from '../test/performance.js';
describe('UInt', (): void => {
const registry = new TypeRegistry();
it('fails on > MAX_SAFE_INTEGER and float', (): void => {
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision, no-loss-of-precision
expect(() => new UInt(registry, 9007199254740999)).toThrow(/integer <= Number.MAX_SAFE_INTEGER/);
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision, no-loss-of-precision
expect(() => new UInt(registry, -9007199254740999)).toThrow(/integer <= Number.MAX_SAFE_INTEGER/);
expect(() => new UInt(registry, 9.123)).toThrow(/integer <= Number.MAX_SAFE_INTEGER/);
});
it('fails on strings with decimal points & scientific notation', (): void => {
expect(() => new UInt(registry, '123.4')).toThrow(/not contain decimal points/);
expect(() => new UInt(registry, '9e10')).toThrow(/not contain decimal points/);
});
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(),
new UInt(registry, 64).divn(2).toNumber()
]).toEqual([64, 32]);
});
it('is a BN instance', (): void => {
const test = new UInt(registry, 16);
expect(isBn(test)).toBe(true);
expect(BN.isBN(test)).toBe(true);
expect(test instanceof BN).toBe(true);
});
// e.g. headers
it('decodes hex that are not multiples of 2', (): void => {
expect(new UInt(registry, '0x123').toNumber()).toEqual(0x123);
expect(new UInt(registry, '0x0123').toNumber()).toEqual(0x123);
});
it('fails on a number that is too large for the bits specified', (): void => {
expect(
(): UInt => new UInt(registry, '12345678901234567890123456789012345678901234567890', 32)
).toThrow('u32: Input too large. Found input with 164 bits, expected 32');
});
it('fails on negative numbers', (): void => {
expect(
(): UInt => new UInt(registry, -123, 32)
).toThrow('u32: Negative number passed to unsigned type');
});
it('allows for construction via BigInt', (): void => {
expect(
new UInt(registry, 123456789123456789123456789n, 128).toHuman()
).toEqual('123,456,789,123,456,789,123,456,789');
});
it('converts to Little Endian from the provided value', (): void => {
expect(
new UInt(registry, 1234567).toU8a()
).toEqual(new Uint8Array([135, 214, 18, 0, 0, 0, 0, 0]));
});
it('converts to Little Endian from the provided value (bitLength)', (): void => {
expect(
new UInt(registry, 1234567, 32).toU8a()
).toEqual(new Uint8Array([135, 214, 18, 0]));
});
it('converts to hex/string', (): void => {
const u = new UInt(registry, '0x12', 16);
expect(u.toHex()).toEqual('0x0012');
expect(u.toString()).toEqual('18');
});
it('converts to equivalents', (): void => {
const a = new UInt(registry, '123');
expect(
new UInt(registry, a).toNumber()
).toEqual(123);
});
it('converts to JSON representation based on size', (): void => {
expect(new UInt(registry, '0x12345678', 32).toJSON()).toEqual(0x12345678);
expect(new UInt(registry, '0x1234567890', 64).toJSON()).toEqual(78187493520);
expect(new UInt(registry, '0x1234567890abcdef', 64).toJSON()).toEqual('0x1234567890abcdef');
expect(new UInt(registry, 1, 256).toJSON()).toEqual('0x0000000000000000000000000000000000000000000000000000000000000001');
});
describe('utilities', (): void => {
it('provides a toBigInt interface', (): void => {
expect(
new UInt(registry, 9876543210123456789n).toBigInt()
).toEqual(9876543210123456789n);
});
it('provides a toBn interface', (): void => {
expect(
new UInt(registry, 987).toBn().toNumber()
).toEqual(987);
});
it('provides a toNumber interface', (): void => {
expect(
new UInt(registry, 4567).toNumber()
).toEqual(4567);
});
it('has a working toBigInt', (): void => {
expect(
new UInt(registry, 4567).toBigInt() + BigInt(1)
).toEqual(BigInt(4568));
});
it('has a sane inspect', (): void => {
expect(
new UInt(registry, '0x12', 16).inspect()
).toEqual({
outer: [new Uint8Array([0x12, 0x00])]
});
});
describe('eq', (): void => {
const test = new UInt(registry, 12345);
it('compares against other BN values', (): void => {
expect(test.eq(new BN(12345))).toBe(true);
});
it('compares against other number values', (): void => {
expect(test.eq(12345)).toBe(true);
});
it('compares against hex values', (): void => {
expect(test.eq('0x3039')).toBe(true);
});
});
describe('isMax()', (): void => {
it('is false where not full', (): void => {
expect(new UInt(registry, '0x1234', 32).isMax()).toEqual(false);
expect(new UInt(registry, '0xffffff', 32).isMax()).toEqual(false);
expect(new UInt(registry, '0x12345678', 32).isMax()).toEqual(false);
expect(new UInt(registry, '0xfffffff0', 32).isMax()).toEqual(false);
});
it('is true when full', (): void => {
expect(new UInt(registry, '0xffffffff', 32).isMax()).toEqual(true);
});
});
});
describe('static with', (): void => {
it('allows default toRawType', (): void => {
expect(
new (UInt.with(64))(registry).toRawType()
).toEqual('u64');
});
it('allows toRawType override', (): void => {
expect(
new (UInt.with(64, 'SomethingElse'))(registry).toRawType()
).toEqual('SomethingElse');
});
it('has proper toHuman() for PerMill/PerBill/Percent/Balance', (): void => {
expect(registry.createType('Perbill', 12_340_000).toHuman()).toEqual('1.23%');
expect(registry.createType('Percent', 12).toHuman()).toEqual('12.00%');
expect(registry.createType('Permill', 16_900).toHuman()).toEqual('1.69%');
expect(registry.createType('Balance', '123456789012345').toHuman()).toEqual('123.4567 Unit');
});
});
perf('UInt', 75_000, [[new Uint8Array([31, 32, 33, 34])]], (v: Uint8Array) => new UInt(registry, v));
});
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyNumber, CodecClass, Registry, UIntBitLength } from '../types/index.js';
import { AbstractInt } from '../abstract/Int.js';
/**
* @name UInt
* @description
* A generic unsigned integer codec. For Bizinikiwi all numbers are Little Endian encoded,
* this handles the encoding and decoding of those numbers. Upon construction
* the bitLength is provided and any additional use keeps the number to this
* length. This extends `BN`, so all methods available on a normal `BN` object
* is available here.
* @noInheritDoc
*/
export class UInt extends AbstractInt {
public static with (bitLength: UIntBitLength, typeName?: string): CodecClass<UInt> {
return class extends UInt {
constructor (registry: Registry, value?: AnyNumber | null) {
super(registry, value, bitLength);
}
public override toRawType (): string {
return typeName || super.toRawType();
}
};
}
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { PropIndex } from '@pezkuwi/types/interfaces/democracy';
import type { Codec, CodecTo, ITuple } from '@pezkuwi/types-codec/types';
import { createTypeUnsafe, GenericAccountId as AccountId, Metadata, TypeRegistry } from '@pezkuwi/types';
import { Text, u32, Vec } from '@pezkuwi/types-codec';
import rpcMetadata from '@pezkuwi/types-support/metadata/static-bizinikiwi';
import { decodeAddress, randomAsU8a } from '@pezkuwi/util-crypto';
import { perf } from '../test/performance.js';
const registry = new TypeRegistry();
const metadata = new Metadata(registry, rpcMetadata);
const VecU32 = Vec.with(u32);
registry.setMetadata(metadata);
describe('Vec', (): void => {
let vector: Vec<Codec>;
beforeEach((): void => {
vector = new Vec(registry, Text, ['1', '23', '345', '4567', new Text(registry, '56789')]);
});
describe('constructor', (): void => {
it('fails construction on non-Array, non-Hex inputs', (): void => {
// @ts-expect-error We are intentionally passing a non-valid input
expect(() => new Vec(registry, Text, '12345')).toThrow(/decoding, found string/);
// @ts-expect-error We are intentionally passing a non-valid input
expect(() => new Vec(registry, Text, {})).toThrow(/decoding, found object/);
});
it('allows construction via hex & null values', (): void => {
// @ts-expect-error We are intentionally passing a non-valid input
expect(new Vec(registry, Text, null)).toHaveLength(0);
});
it('decodes a complex type via construction (1)', (): void => {
const test = createTypeUnsafe<Vec<ITuple<[PropIndex, AccountId]>>>(registry, 'Vec<(PropIndex, AccountId)>', [new Uint8Array([
4, 10, 0, 0, 0, 209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79
])]);
expect(test[0][0].toNumber()).toEqual(10);
expect(test[0][1].toString()).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
});
it('decodes a complex type via construction (2)', (): void => {
const INPUT = '0x08cc0200000000ce0200000001';
const test = createTypeUnsafe<Vec<Codec>>(registry, 'Vec<(u32, [u32; 0], u16)>', [INPUT]);
expect(test).toHaveLength(2);
expect(test.toHex()).toEqual(INPUT);
});
it('allows construction via JSON', (): void => {
expect(
new Vec(registry, Text, ['6', '7']).toJSON()
).toEqual(['6', '7']);
});
it('allows construction via JSON (string type)', (): void => {
expect(
new Vec(registry, 'u32', ['6', '7']).toJSON()
).toEqual([6, 7]);
});
it('decodes reusing instantiated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new Vec(registry, Text, [foo]))[0]
).toBe(foo);
});
});
describe('vector-like functions', (): void => {
it('wraps a sequence of values', (): void => {
expect(vector).toHaveLength(5);
});
it('has a sane representation for toString', (): void => {
expect(vector.toString()).toEqual('[1, 23, 345, 4567, 56789]');
});
it('encodes with length prefix on toU8a()', (): void => {
expect(vector.toU8a()).toEqual(new Uint8Array([
5 << 2,
1 << 2, 49,
2 << 2, 50, 51,
3 << 2, 51, 52, 53,
4 << 2, 52, 53, 54, 55,
5 << 2, 53, 54, 55, 56, 57
]));
});
it('encodes without length prefix on toU8a(true)', (): void => {
expect(vector.toU8a(true)).toEqual(new Uint8Array([
1 << 2, 49,
2 << 2, 50, 51,
3 << 2, 51, 52, 53,
4 << 2, 52, 53, 54, 55,
5 << 2, 53, 54, 55, 56, 57
]));
});
it('exposes the type', (): void => {
expect(vector.Type).toEqual('Text');
});
it('allows retrieval of a specific item', (): void => {
expect(
vector[2].toString()
).toEqual('345');
});
it('exposes a working forEach', (): void => {
const result: Record<number, string> = {};
vector.forEach((e, i): void => {
result[i] = e.toString();
});
expect(result).toEqual({
0: '1',
1: '23',
2: '345',
3: '4567',
4: '56789'
});
});
it('exposes a working concat', (): void => {
expect(
vector.concat(new Vec(registry, Text, ['987', '654'])).toString()
).toEqual('1,23,345,4567,56789,987,654');
});
it('exposes a working filter', (): void => {
expect(
vector.filter((_, i): boolean => i >= 3).toString()
).toEqual('4567,56789');
});
it('exposes a working map', (): void => {
expect(
vector.map((e): string => e.toString().substring(0, 1))
).toEqual(['1', '2', '3', '4', '5']);
});
it('exposes a working reduce', (): void => {
expect(
vector.reduce((r, e): string => `${r}${e.toString()}`, '')
).toEqual('123345456756789');
});
it('exposes a working indexOf', (): void => {
expect(vector.indexOf('1')).toEqual(0);
expect(vector.indexOf(new Text(registry, '23'))).toEqual(1);
expect(vector.indexOf('0')).toEqual(-1);
});
});
describe('encode', (): void => {
const testEncode = (to: CodecTo, expected: any): void =>
it(`can encode ${to}`, (): void => {
expect(vector[to]()).toEqual(expected);
});
testEncode('toHex', '0x1404310832330c3334351034353637143536373839');
testEncode('toJSON', ['1', '23', '345', '4567', '56789']);
testEncode('toString', '[1, 23, 345, 4567, 56789]');
testEncode('toU8a', Uint8Array.from([20, 4, 49, 8, 50, 51, 12, 51, 52, 53, 16, 52, 53, 54, 55, 20, 53, 54, 55, 56, 57]));
});
describe('utils', (): void => {
const vec = new Vec(registry, Text, ['123', '456']);
it('compares against codec types', (): void => {
expect(vec.eq([new Text(registry, '123'), new Text(registry, '456')])).toBe(true);
});
it('compares against codec + primitive types', (): void => {
expect(vec.eq(['123', new Text(registry, '456')])).toBe(true);
});
it('finds the index of an value', (): void => {
const myId = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const vec = new Vec(registry, AccountId, [
'5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'
]);
expect(vec.indexOf(myId)).toEqual(2);
});
it('allows a slice operator', (): void => {
const vec = registry.createType('Vec<AccountId>', [
randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a(), randomAsU8a()
]);
expect(vec).toHaveLength(10);
expect(vec.slice(2, 7)).toHaveLength(5);
});
it('has a sane inspect', (): void => {
const addrs = [
'5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'
];
const vec = registry.createType('Vec<AccountId>', addrs);
expect(vec.inspect()).toEqual({
inner: addrs.map((a) => ({
outer: [decodeAddress(a)]
})),
outer: [new Uint8Array([3 << 2])]
});
});
});
perf('Vec<U32>', 40_000, [[new Uint8Array([3 << 2, 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34])]], (v: Uint8Array) => new VecU32(registry, v));
});
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { Codec, CodecClass, DefinitionSetter, Registry } from '../types/index.js';
import { compactFromU8aLim, identity, isHex, isU8a, logger, stringify, u8aToU8a } from '@pezkuwi/util';
import { AbstractArray } from '../abstract/Array.js';
import { decodeU8aVec, typeToConstructor } from '../utils/index.js';
const MAX_LENGTH = 512 * 1024;
const l = logger('Vec');
function decodeVecLength (value: Uint8Array | HexString | unknown[]): [Uint8Array | unknown[] | null, number, number] {
if (Array.isArray(value)) {
return [value, value.length, 0];
} else if (isU8a(value) || isHex(value)) {
const u8a = u8aToU8a(value);
const [startAt, length] = compactFromU8aLim(u8a);
if (length > MAX_LENGTH) {
throw new Error(`Vec length ${length.toString()} exceeds ${MAX_LENGTH}`);
}
return [u8a, length, startAt];
} else if (!value) {
return [null, 0, 0];
}
throw new Error(`Expected array/hex input to Vec<*> decoding, found ${typeof value}: ${stringify(value)}`);
}
export function decodeVec<T extends Codec> (registry: Registry, result: T[], value: Uint8Array | HexString | unknown[] | null, startAt: number, Type: CodecClass<T>): [number, number] {
if (Array.isArray(value)) {
const count = result.length;
for (let i = 0; i < count; i++) {
// 26/08/2022 this is actually a false positive - after recent eslint upgdates
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const entry = value[i];
try {
result[i] = entry instanceof Type
? entry
: new Type(registry, entry);
} catch (error) {
l.error(`Unable to decode on index ${i}`, (error as Error).message);
throw error;
}
}
return [0, 0];
} else if (!value) {
return [0, 0];
}
// we don't need more checks, we already limited it via the length decoding
return decodeU8aVec(registry, result, u8aToU8a(value), startAt, Type);
}
/**
* @name Vec
* @description
* This manages codec arrays. Internally it keeps track of the length (as decoded) and allows
* construction with the passed `Type` in the constructor. It is an extension to Array, providing
* specific encoding/decoding on top of the base type.
*/
export class Vec<T extends Codec> extends AbstractArray<T> {
#Type: CodecClass<T>;
constructor (registry: Registry, Type: CodecClass<T> | string, value: Uint8Array | HexString | unknown[] = [], { definition, setDefinition = identity }: DefinitionSetter<CodecClass<T>> = {}) {
const [decodeFrom, length, startAt] = decodeVecLength(value);
super(registry, length);
this.#Type = definition || setDefinition(typeToConstructor<T>(registry, Type));
this.initialU8aLength = (
isU8a(decodeFrom)
? decodeU8aVec(registry, this, decodeFrom, startAt, this.#Type)
: decodeVec(registry, this, decodeFrom, startAt, this.#Type)
)[0];
}
public static with<O extends Codec> (Type: CodecClass<O> | string): CodecClass<Vec<O>> {
let definition: CodecClass<O> | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = <T> (d: CodecClass<T>) =>
(definition = d as unknown as CodecClass<O>) as unknown as CodecClass<T>;
return class extends Vec<O> {
constructor (registry: Registry, value?: any[]) {
super(registry, Type, value, { definition, setDefinition });
}
};
}
/**
* @description The type for the items
*/
public get Type (): string {
return this.#Type.name;
}
/**
* @description Finds the index of the value in the array
*/
public override indexOf (other?: unknown): number {
// convert type first, this removes overhead from the eq
const check = other instanceof this.#Type
? other
: new this.#Type(this.registry, other);
for (let i = 0, count = this.length; i < count; i++) {
if (check.eq(this[i])) {
return i;
}
}
return -1;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return `Vec<${this.registry.getClassName(this.#Type) || new this.#Type(this.registry).toRawType()}>`;
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec } from '../types/index.js';
import { AbstractArray } from '../abstract/Array.js';
/**
* @name VecAny
* @description
* This manages codec arrays, assuming that the inputs are already of type Codec. Unlike
* a vector, this can be used to manage array-like structures with variable arguments of
* any types
*/
export class VecAny<T extends Codec> extends AbstractArray<T> {
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
// FIXME This is basically an any type, cannot instantiate via createType
return 'Vec<Codec>';
}
}
@@ -0,0 +1,78 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Text, u16, VecFixed } from '@pezkuwi/types-codec';
import { stringToU8a } from '@pezkuwi/util';
describe('VecFixed', (): void => {
const registry = new TypeRegistry();
describe('construction', (): void => {
it('constructs via empty', (): void => {
expect(new VecFixed(registry, Text, 2).toHex()).toEqual('0x0000');
});
it('constructs via Uint8Array', (): void => {
expect(new VecFixed(registry, Text, 2, new Uint8Array([0x00, 0x04, 0x31])).toHex()).toEqual('0x000431');
});
it('constructs via hex', (): void => {
expect(new VecFixed(registry, u16, 2, '0x12345678').toHex()).toEqual('0x12345678');
});
it('decodes reusing instance inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new VecFixed(registry, Text, 1, [foo]))[0]
).toBe(foo);
});
});
describe('utils', (): void => {
let test: VecFixed<Text>;
beforeEach((): void => {
test = new (VecFixed.with(Text, 5))(registry, ['1', '2', '3', undefined, '56']);
});
it('has a sane string types', (): void => {
expect(test.toRawType()).toEqual('[Text;5]');
expect(test.Type).toEqual('Text');
});
it('has a correct toHex', (): void => {
// each entry length 1 << 2, char as hex (0x31 === `1`), one empty
expect(test.toHex()).toEqual('0x04310432043300083536');
});
it('has empty Uint8Array when length is 0', (): void => {
const test = new (VecFixed.with(Text, 0))(registry);
expect(test.encodedLength).toEqual(0);
expect(test.toU8a()).toEqual(new Uint8Array([]));
});
it('has equivalent to 1 Uint8Array when length is 1', (): void => {
const test = new (VecFixed.with(Text, 1))(registry, ['hello']);
expect(test.encodedLength).toEqual(1 + 5);
expect(test.toU8a()).toEqual(new Uint8Array([20, 104, 101, 108, 108, 111]));
});
it('has a sane inspect', (): void => {
expect(test.inspect()).toEqual({
inner: [
{ outer: [new Uint8Array([1 << 2]), stringToU8a('1')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('2')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('3')] },
{ outer: [new Uint8Array([0])] },
{ outer: [new Uint8Array([2 << 2]), stringToU8a('56')] }
]
});
});
});
});
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { Codec, CodecClass, DefinitionSetter, Inspect, Registry } from '../types/index.js';
import { identity, isU8a, u8aConcatStrict } from '@pezkuwi/util';
import { AbstractArray } from '../abstract/Array.js';
import { decodeU8aVec, typeToConstructor } from '../utils/index.js';
import { decodeVec } from './Vec.js';
/**
* @name VecFixed
* @description
* This manages codec arrays of a fixed length
*/
export class VecFixed<T extends Codec> extends AbstractArray<T> {
#Type: CodecClass<T>;
constructor (registry: Registry, Type: CodecClass<T> | string, length: number, value: Uint8Array | HexString | unknown[] = [] as unknown[], { definition, setDefinition = identity }: DefinitionSetter<CodecClass<T>> = {}) {
super(registry, length);
this.#Type = definition || setDefinition(typeToConstructor<T>(registry, Type));
this.initialU8aLength = (
isU8a(value)
? decodeU8aVec(registry, this, value, 0, this.#Type)
: decodeVec(registry, this, value, 0, this.#Type)
)[1];
}
public static with<O extends Codec> (Type: CodecClass<O> | string, length: number): CodecClass<VecFixed<O>> {
let definition: CodecClass<O> | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = <T> (d: CodecClass<T>) =>
(definition = d as unknown as CodecClass<O>) as unknown as CodecClass<T>;
return class extends VecFixed<O> {
constructor (registry: Registry, value?: any[]) {
super(registry, Type, length, value, { definition, setDefinition });
}
};
}
/**
* @description The type for the items
*/
public get Type (): string {
return new this.#Type(this.registry).toRawType();
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public override get encodedLength (): number {
let total = 0;
for (let i = 0, count = this.length; i < count; i++) {
total += this[i].encodedLength;
}
return total;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (): Inspect {
return {
inner: this.inspectInner()
};
}
public override toU8a (): Uint8Array {
// we override, we don't add the length prefix for ourselves, and at the same time we
// ignore isBare on entries, since they should be properly encoded at all times
const encoded = this.toU8aInner();
return encoded.length
? u8aConcatStrict(encoded)
: new Uint8Array([]);
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return `[${this.Type};${this.length}]`;
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { Compact } from './Compact.js';
export { DoNotConstruct } from './DoNotConstruct.js';
export { Enum } from './Enum.js';
export { Int } from './Int.js';
export { Null } from './Null.js';
export { Option } from './Option.js';
export { Result } from './Result.js';
export { Tuple } from './Tuple.js';
export { UInt } from './UInt.js';
export { Vec } from './Vec.js';
export { VecAny } from './VecAny.js';
export { VecFixed } from './VecFixed.js';
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
// all named
export { packageInfo } from './packageInfo.js';
// all starred
export * from './abstract/index.js';
export * from './base/index.js';
export * from './extended/index.js';
export * from './native/index.js';
export * from './primitive/index.js';
export * from './utils/index.js';
@@ -0,0 +1,12 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/types-augment';
import { TypeRegistry } from '@pezkuwi/types';
import { U32 } from './index.js';
const registry = new TypeRegistry();
console.log(new U32(registry).divn(1));
@@ -0,0 +1,245 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecClass, ITuple } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { BTreeMap, Enum, I32, Option, Struct, Text, Tuple, U32 } from '@pezkuwi/types-codec';
import { stringToU8a } from '@pezkuwi/util';
const registry = new TypeRegistry();
class U32TextTuple extends (Tuple.with([U32, Text]) as unknown as CodecClass<ITuple<[U32, Text]>>) {}
// Reason: We purposefully want `text` to be the first key of the struct and take priority during sorting
// eslint-disable-next-line sort-keys
class MockStruct extends Struct.with({ text: Text, int: I32 }) {}
class MockEnum extends Enum.with({
Key1: MockStruct,
Key2: MockStruct,
Key3: U32TextTuple
}) {}
class MockOptionEnum extends Option.with(MockEnum) {}
const mockU32TextMap = new Map<Text, U32>();
const mockU32DuplicateTextMap = new Map<Text, U32>();
const mockU32TupleMap = new Map<ITuple<[U32, Text]>, U32>();
const mockU32I32Map = new Map<I32, U32>();
const mockU32StructMap = new Map<MockStruct, U32>();
const mockU32EnumMap = new Map<MockEnum, U32>();
const mockU32OptionEnumMap = new Map<MockOptionEnum, U32>();
mockU32TextMap.set(new Text(registry, 'bazzing'), new U32(registry, 69));
mockU32DuplicateTextMap.set(new Text(registry, 'bazzing'), new U32(registry, 42));
mockU32DuplicateTextMap.set(new Text(registry, 'bazzing'), new U32(registry, 43));
mockU32TupleMap.set((new U32TextTuple(registry, [2, 'ba'])), new U32(registry, 42));
mockU32TupleMap.set((new U32TextTuple(registry, [2, 'b'])), new U32(registry, 7));
mockU32TupleMap.set((new U32TextTuple(registry, [1, 'baz'])), new U32(registry, 13));
mockU32I32Map.set(new I32(registry, 255), new U32(registry, 69));
mockU32I32Map.set(new I32(registry, -255), new U32(registry, 42));
mockU32I32Map.set(new I32(registry, 1000), new U32(registry, 7));
mockU32I32Map.set(new I32(registry, -1000), new U32(registry, 25));
mockU32I32Map.set(new I32(registry, 0), new U32(registry, 13));
mockU32StructMap.set(new MockStruct(registry, { int: 1, text: 'b' }), new U32(registry, 42));
mockU32StructMap.set(new MockStruct(registry, { int: -1, text: 'b' }), new U32(registry, 7));
mockU32StructMap.set(new MockStruct(registry, { int: -1, text: 'ba' }), new U32(registry, 25));
mockU32StructMap.set(new MockStruct(registry, { int: -2, text: 'baz' }), new U32(registry, 13));
mockU32EnumMap.set(new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'ba']) }), new U32(registry, 13));
mockU32EnumMap.set(new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'b']) }), new U32(registry, 42));
mockU32EnumMap.set(new MockEnum(registry, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 7));
mockU32EnumMap.set(new MockEnum(registry, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }), new U32(registry, 25));
mockU32EnumMap.set(new MockEnum(registry, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 69));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, null), new U32(registry, 13));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, { Key3: new U32TextTuple(registry, [2, 'ba']) }), new U32(registry, 13));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, { Key3: new U32TextTuple(registry, [2, 'b']) }), new U32(registry, 42));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 7));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }), new U32(registry, 25));
mockU32OptionEnumMap.set(new Option(registry, MockEnum, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 69));
describe('BTreeMap', (): void => {
it('decodes null', (): void => {
expect(
new (
BTreeMap.with(Text, U32)
)(registry, null).toString()
).toEqual('{}');
});
it('decodes reusing instantiated inputs', (): void => {
const key = new Text(registry, 'foo');
const val = new Text(registry, 'bar');
expect(
(new (BTreeMap.with(Text, Text))(registry, new Map([[key, val]]))).eq(new Map([[key, val]]))
).toBe(true);
});
it('decodes within more complicated types', (): void => {
const s = new Struct(registry, {
placeholder: U32,
value: 'BTreeMap<Text, U32>'
});
s.set('value', new (BTreeMap.with(Text, U32))(registry, mockU32TextMap));
expect(s.toString()).toBe('{"placeholder":0,"value":{"bazzing":69}}');
});
it('throws on duplicate keys', (): void => {
expect(
() => new (BTreeMap.with(Text, U32))(registry, mockU32DuplicateTextMap)
).toThrow(/Duplicate value in BTreeMap/);
});
it('throws when it cannot decode', (): void => {
expect(
(): BTreeMap<Text, U32> => new (
BTreeMap.with(Text, U32)
)(registry, 'ABC')
).toThrow(/Map: cannot decode type/);
});
it('correctly encodes length', (): void => {
expect(
new (
BTreeMap.with(Text, U32))(registry, mockU32TextMap).encodedLength
).toEqual(13);
});
it('correctly sorts simple keys', (): void => {
expect(
Array.from(new (BTreeMap.with(I32, U32))(registry, mockU32I32Map).keys()).map((k) => k.toNumber())
).toEqual([-1000, -255, 0, 255, 1000]);
});
it('correctly sorts tuple keys', (): void => {
expect(
Array.from(new (BTreeMap.with(U32TextTuple, U32))(registry, mockU32TupleMap).keys()).map((k) => k.toJSON())
).toEqual([[1, 'baz'], [2, 'b'], [2, 'ba']]);
});
it('correctly sorts struct keys', (): void => {
expect(
Array.from(new (BTreeMap.with(MockStruct, U32))(registry, mockU32StructMap).keys()).map((k) => k.toJSON())
).toEqual([
{ int: -1, text: 'b' },
{ int: 1, text: 'b' },
{ int: -1, text: 'ba' },
{ int: -2, text: 'baz' }
]);
});
it('correctly sorts Option(Enum) keys', (): void => {
expect(
Array.from(new (BTreeMap.with(MockOptionEnum, U32))(registry, mockU32OptionEnumMap).keys()).map((k) => k.value.toJSON())
).toEqual([
null,
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
it('correctly sorts enum keys', (): void => {
expect(
Array.from(new (BTreeMap.with(MockEnum, U32))(registry, mockU32EnumMap).keys()).map((k) => k.toJSON())
).toEqual([
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
it('correctly serializes/deserializes to/from json with numeric keys', (): void => {
expect(
new (BTreeMap.with(I32, U32))(
registry,
new (BTreeMap.with(I32, U32))(registry, mockU32I32Map).toJSON()
).toJSON()
).toEqual({ '-1000': 25, '-255': 42, 0: 13, 1000: 7, 255: 69 });
});
it('correctly serializes/deserializes to/from json with text keys', (): void => {
expect(
new (BTreeMap.with(Text, U32))(
registry,
new (BTreeMap.with(Text, U32))(registry, mockU32TextMap).toJSON()
).toJSON()
).toEqual({ bazzing: 69 });
});
it('correctly serializes/deserializes to/from json with tuple keys', (): void => {
expect(
new (BTreeMap.with(U32TextTuple, U32))(
registry,
new (BTreeMap.with(U32TextTuple, U32))(registry, mockU32TupleMap).toJSON()
).toJSON()
).toEqual({ '[1,"baz"]': 13, '[2,"b"]': 7, '[2,"ba"]': 42 });
});
it('correctly serializes/deserializes to/from json with struct keys', (): void => {
expect(
new (BTreeMap.with(MockStruct, U32))(
registry,
new (BTreeMap.with(MockStruct, U32))(registry, mockU32StructMap).toJSON()
).toJSON()
).toEqual({
'{"text":"b","int":-1}': 7,
'{"text":"b","int":1}': 42,
'{"text":"ba","int":-1}': 25,
'{"text":"baz","int":-2}': 13
});
});
it('correctly serializes/deserializes to/from json with enum keys', (): void => {
expect(
new (BTreeMap.with(MockEnum, U32))(
registry,
new (BTreeMap.with(MockEnum, U32))(registry, mockU32EnumMap).toJSON()
).toJSON()
).toEqual({
'{"key1":{"text":"b","int":-1}}': 69,
'{"key1":{"text":"b","int":1}}': 25,
'{"key2":{"text":"b","int":-1}}': 7,
'{"key3":[2,"b"]}': 42,
'{"key3":[2,"ba"]}': 13
});
});
it('generates sane toRawTypes', (): void => {
expect(new (BTreeMap.with(Text, U32))(registry).toRawType()).toBe('BTreeMap<Text,u32>');
expect(new (BTreeMap.with(Text, Text))(registry).toRawType()).toBe('BTreeMap<Text,Text>');
expect(new (BTreeMap.with(Text, Struct.with({ a: U32, b: Text })))(registry).toRawType())
.toBe('BTreeMap<Text,{"a":"u32","b":"Text"}>');
});
it('has a sane inspect', (): void => {
expect(
new (BTreeMap.with(Text, Text))(registry, new Map([
[new Text(registry, '1'), new Text(registry, 'foo')],
[new Text(registry, '2'), new Text(registry, 'bar')],
[new Text(registry, '3'), new Text(registry, 'baz')]
])).inspect()
).toEqual({
inner: [
{ outer: [new Uint8Array([1 << 2]), stringToU8a('1')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('foo')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('2')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('bar')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('3')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('baz')] }
],
outer: [new Uint8Array([3 << 2])]
});
});
});
@@ -0,0 +1,16 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec, CodecClass, Registry } from '../types/index.js';
import { CodecMap } from './Map.js';
export class BTreeMap<K extends Codec = Codec, V extends Codec = Codec> extends CodecMap<K, V> {
public static with<K extends Codec, V extends Codec> (keyType: CodecClass<K> | string, valType: CodecClass<V> | string): CodecClass<CodecMap<K, V>> {
return class extends BTreeMap<K, V> {
constructor (registry: Registry, value?: Uint8Array | string | Map<any, any>) {
super(registry, keyType, valType, value, 'BTreeMap');
}
};
}
}
@@ -0,0 +1,260 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecClass, CodecTo } from '@pezkuwi/types-codec/types';
import type { ITuple } from '../types/interfaces.js';
import { TypeRegistry } from '@pezkuwi/types';
import { BTreeSet, Enum, I32, Option, Struct, Text, Tuple, U32 } from '@pezkuwi/types-codec';
const registry = new TypeRegistry();
class U32TextTuple extends (Tuple.with([U32, Text]) as unknown as CodecClass<ITuple<[U32, Text]>>) {}
// Reason: We purposefully want `text` to be the first key of the struct and take priority during sorting
// eslint-disable-next-line sort-keys
class MockStruct extends Struct.with({ text: Text, int: I32 }) {}
class MockEnum extends Enum.with({
Key1: MockStruct,
Key2: MockStruct,
Key3: U32TextTuple
}) {}
class MockOptionEnum extends Option.with(MockEnum) {}
const mockU32Set = new Set<U32>();
mockU32Set.add(new U32(registry, 2));
mockU32Set.add(new U32(registry, 24));
mockU32Set.add(new U32(registry, 30));
mockU32Set.add(new U32(registry, 80));
const mockU32SetString = '[2,24,30,80]';
const mockU32SetObject = [2, 24, 30, 80];
const mockU32SetHexString = '0x1002000000180000001e00000050000000';
const mockU32SetUint8Array = Uint8Array.from([16, 2, 0, 0, 0, 24, 0, 0, 0, 30, 0, 0, 0, 80, 0, 0, 0]);
const mockI32SetObj = [1000, 0, 255, -255, -1000];
const mockTextSetObj = [
new Text(registry, 'baz'),
new Text(registry, 'b'),
new Text(registry, 'bb'),
new Text(registry, 'ba'),
new Text(registry, 'c')
];
const mockTupleSetObj = [
new U32TextTuple(registry, [2, 'ba']),
new U32TextTuple(registry, [2, 'bb']),
new U32TextTuple(registry, [2, 'b']),
new U32TextTuple(registry, [1, 'baz'])
];
const mockStructSetObj = [
new MockStruct(registry, { int: 1, text: 'b' }),
new MockStruct(registry, { int: -1, text: 'b' }),
new MockStruct(registry, { int: -1, text: 'ba' }),
new MockStruct(registry, { int: -2, text: 'baz' })
];
const mockEnumSetObj = [
new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'ba']) }),
new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'b']) }),
new MockEnum(registry, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }),
new MockEnum(registry, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }),
new MockEnum(registry, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) })
];
const mockOptionEnumSetObj = [
new Option(registry, MockEnum, null),
new Option(registry, MockEnum, { Key3: new U32TextTuple(registry, [2, 'ba']) }),
new Option(registry, MockEnum, { Key3: new U32TextTuple(registry, [2, 'b']) }),
new Option(registry, MockEnum, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }),
new Option(registry, MockEnum, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }),
new Option(registry, MockEnum, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) })
];
const mockDuplicateTextSetObj = [
new Text(registry, 'baz'),
new Text(registry, 'bb'),
new Text(registry, 'bb'), // duplicate.
new Text(registry, 'ba'),
new Text(registry, 'c')
];
describe('BTreeSet', (): void => {
describe('decoding', (): void => {
const testDecode = (type: string, input: unknown, output: string): void =>
it(`can decode from ${type}`, (): void => {
const s = new BTreeSet(registry, U32, input as string);
expect(s.toString()).toBe(output);
});
testDecode('Set', mockU32Set, mockU32SetString);
testDecode('hex', mockU32SetHexString, mockU32SetString);
testDecode('Uint8Array', mockU32SetUint8Array, mockU32SetString);
testDecode('Set', mockU32Set, mockU32SetString);
testDecode('hex', mockU32SetHexString, mockU32SetString);
testDecode('Uint8Array', mockU32SetUint8Array, mockU32SetString);
});
describe('encoding multiple values', (): void => {
const testEncode = (to: CodecTo, expected: any): void =>
it(`can encode ${to}`, (): void => {
const s = new BTreeSet(registry, U32, mockU32Set);
expect(s[to]()).toEqual(expected);
});
testEncode('toHex', mockU32SetHexString);
testEncode('toJSON', mockU32SetObject);
testEncode('toU8a', mockU32SetUint8Array);
testEncode('toString', mockU32SetString);
});
it('decodes null', (): void => {
expect(
new (
BTreeSet.with(U32)
)(registry, null).toString()
).toEqual('[]');
});
it('decodes reusing instantiated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new BTreeSet(registry, Text, new Set([foo]))).eq(new Set([foo]))
).toBe(true);
});
it('decodes within more complicated types', (): void => {
const s = new Struct(registry, {
placeholder: U32,
value: BTreeSet.with(U32)
});
s.set('value', new BTreeSet(registry, U32, mockU32Set));
expect(s.toString()).toBe('{"placeholder":0,"value":[2,24,30,80]}');
});
it('throws when it cannot decode', (): void => {
expect(
(): BTreeSet<U32> => new (
BTreeSet.with(U32)
)(registry, 'ABC')
).toThrow(/BTreeSet: cannot decode type/);
});
describe('enocodedLength & initialU8aLength', (): void => {
it('correctly encodes length', (): void => {
expect(
new (
BTreeSet.with(U32))(registry, mockU32Set).encodedLength
).toEqual(17);
});
it('correctly encodes/decodes empty', (): void => {
const none = new (BTreeSet.with(U32))(registry, []);
// only the length byte
expect(none.toHex()).toEqual('0x00');
expect(none.encodedLength).toEqual(1);
expect(
(new (BTreeSet.with(U32))(registry, none.toHex())).initialU8aLength
).toEqual(none.encodedLength);
});
it('correctly encodes/decodes filled', (): void => {
const some = new (BTreeSet.with(U32))(registry, [1, 2]);
// length byte + 2 values, 2 << 2 with u32 values
expect(some.toHex()).toEqual('0x080100000002000000');
expect(some.encodedLength).toEqual(1 + (4 * 2));
expect(
(new (BTreeSet.with(U32))(registry, some.toHex())).initialU8aLength
).toEqual(some.encodedLength);
});
});
describe('sorting', (): void => {
it('correctly sorts numeric values', (): void => {
expect(
Array.from(new (BTreeSet.with(I32))(registry, mockI32SetObj)).map((k) => k.toNumber())
).toEqual([-1000, -255, 0, 255, 1000]);
});
it('correctly sorts text values', (): void => {
expect(
Array.from(new (BTreeSet.with(Text))(registry, mockTextSetObj)).map((k) => k.toString())
).toEqual(['b', 'ba', 'baz', 'bb', 'c']);
});
it('Reject duplicate values', (): void => {
expect(
() => new (BTreeSet.with(Text))(registry, mockDuplicateTextSetObj)
).toThrow(/Duplicate value in BTreeSet/);
});
it('correctly sorts complex tuple values', (): void => {
expect(
Array.from(new (BTreeSet.with(U32TextTuple))(registry, mockTupleSetObj)).map((k) => k.toJSON())
).toEqual([[1, 'baz'], [2, 'b'], [2, 'ba'], [2, 'bb']]);
});
it('correctly sorts complex struct values', (): void => {
expect(
Array.from(new (BTreeSet.with(MockStruct))(registry, mockStructSetObj)).map((k) => k.toJSON())
).toEqual([
{ int: -1, text: 'b' },
{ int: 1, text: 'b' },
{ int: -1, text: 'ba' },
{ int: -2, text: 'baz' }
]);
});
it('correctly sorts complex Option(enum) values', (): void => {
expect(
Array.from(new (BTreeSet.with(MockOptionEnum))(registry, mockOptionEnumSetObj)).map((k) => k.value.toJSON())
).toEqual([
null,
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
it('correctly sorts complex enum values', (): void => {
expect(
Array.from(new (BTreeSet.with(MockEnum))(registry, mockEnumSetObj)).map((k) => k.toJSON())
).toEqual([
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
});
it('generates sane toRawTypes', (): void => {
expect(new (BTreeSet.with(U32))(registry).toRawType()).toBe('BTreeSet<u32>');
expect(new (BTreeSet.with(Text))(registry).toRawType()).toBe('BTreeSet<Text>');
expect(new (BTreeSet.with(Struct.with({ a: U32, b: Text })))(registry).toRawType())
.toBe('BTreeSet<{"a":"u32","b":"Text"}>');
});
it('has a sane inspect', (): void => {
expect(
new (BTreeSet.with(U32))(registry, [1, 2, 3, 4]).inspect()
).toEqual({
inner: [
{ outer: [new Uint8Array([1, 0, 0, 0])] },
{ outer: [new Uint8Array([2, 0, 0, 0])] },
{ outer: [new Uint8Array([3, 0, 0, 0])] },
{ outer: [new Uint8Array([4, 0, 0, 0])] }
],
outer: [new Uint8Array([4 << 2])]
});
});
});
@@ -0,0 +1,233 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, CodecClass, Inspect, ISet, IU8a, Registry } from '../types/index.js';
import { compactFromU8aLim, compactToU8a, isHex, isU8a, logger, stringify, u8aConcatStrict, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { compareSet, decodeU8aVec, sortSet, typeToConstructor } from '../utils/index.js';
const l = logger('BTreeSet');
/** @internal */
function decodeSetFromU8a<V extends Codec> (registry: Registry, ValClass: CodecClass<V>, u8a: Uint8Array): [CodecClass<V>, Set<V>, number] {
const output = new Set<V>();
const [offset, count] = compactFromU8aLim(u8a);
const result = new Array<V>(count);
const [decodedLength] = decodeU8aVec(registry, result, u8a, offset, ValClass);
for (let i = 0; i < count; i++) {
output.add(result[i] as unknown as V);
}
return [ValClass, output, decodedLength];
}
/** @internal */
function decodeSetFromSet<V extends Codec> (registry: Registry, ValClass: CodecClass<V>, value: Set<any> | string[]): [CodecClass<V>, Set<V>, number] {
const output = new Set<V>();
value.forEach((val: any) => {
try {
output.add((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 [ValClass, output, 0];
}
/**
* Decode input to pass into constructor.
*
* @param ValClass - Type of the map value
* @param value - Value to decode, one of:
* - null
* - undefined
* - hex
* - Uint8Array
* - Set<any>, where both key and value types are either
* constructors or decodeable values for their types.
* @param jsonSet
* @internal
*/
function decodeSet<V extends Codec> (registry: Registry, valType: CodecClass<V> | string, value?: Uint8Array | string | string[] | Set<any>): [CodecClass<V>, Set<V>, number] {
const ValClass = typeToConstructor(registry, valType);
if (!value) {
return [ValClass, new Set<V>(), 0];
} else if (isU8a(value) || isHex(value)) {
return decodeSetFromU8a<V>(registry, ValClass, u8aToU8a(value));
} else if (Array.isArray(value) || value instanceof Set) {
return decodeSetFromSet<V>(registry, ValClass, value);
}
throw new Error('BTreeSet: cannot decode type');
}
export class BTreeSet<V extends Codec = Codec> extends Set<V> implements ISet<V> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #ValClass: CodecClass<V>;
constructor (registry: Registry, valType: CodecClass<V> | string, rawValue?: Uint8Array | string | string[] | Set<any>) {
const [ValClass, values, decodedLength] = decodeSet(registry, valType, rawValue);
super(sortSet(values));
this.registry = registry;
this.initialU8aLength = decodedLength;
this.#ValClass = ValClass;
}
public static with<V extends Codec> (valType: CodecClass<V> | string): CodecClass<BTreeSet<V>> {
return class extends BTreeSet<V> {
constructor (registry: Registry, value?: Uint8Array | string | Set<any>) {
super(registry, valType, value);
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
let len = compactToU8a(this.size).length;
for (const v of this.values()) {
len += v.encodedLength;
}
return len;
}
/**
* @description Returns a hash of the value
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.size === 0;
}
/**
* @description The actual set values as a string[]
*/
public get strings (): string[] {
return [...super.values()].map((v) => v.toString());
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return compareSet(this, other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
const inner: Inspect[] = [];
for (const v of this.values()) {
inner.push(v.inspect());
}
return {
inner,
outer: [compactToU8a(this.size)]
};
}
/**
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
*/
public toHex (): HexString {
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, disableAscii?: boolean): AnyJson {
const json: AnyJson = [];
for (const v of this.values()) {
json.push(v.toHuman(isExtended, disableAscii));
}
return json;
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
const json: AnyJson = [];
for (const v of this.values()) {
json.push(v.toJSON());
}
return json;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return `BTreeSet<${this.registry.getClassName(this.#ValClass) || new this.#ValClass(this.registry).toRawType()}>`;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
const json: AnyJson = [];
for (const v of this.values()) {
json.push(v.toPrimitive(disableAscii));
}
return json;
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return 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: Uint8Array[] = [];
if (!isBare) {
encoded.push(compactToU8a(this.size));
}
for (const v of this.values()) {
encoded.push(v.toU8a(isBare));
}
return u8aConcatStrict(encoded);
}
}
@@ -0,0 +1,97 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { hexToU8a } from '@pezkuwi/util';
import { BitVec } from './index.js';
// form Inclusion BitVec<lsb0, u8>
const TESTS = ['0x00', '0x0817', '0x0837', '0x087b', '0x0c33'];
const registry = new TypeRegistry();
describe('BitVec', (): void => {
describe('decoding known', (): void => {
TESTS.forEach((test): void => {
describe(`${test}`, (): void => {
const input = hexToU8a(test);
const bitvec = new BitVec(registry, input);
it('has the right encodedLength', (): void => {
expect(
bitvec.encodedLength
).toEqual((test.length - 2) / 2);
});
it('re-encodes to the same input value', (): void => {
expect(
bitvec.toU8a()
).toEqual(input);
});
});
});
});
describe('toHuman() ordering', (): void => {
it('defaults to Lsb', (): void => {
expect(
new BitVec(registry, '0x0100010500').toHuman()
).toEqual('0b10000000_00000000_10000000_10100000_00000000');
});
it('can output to Msb', (): void => {
expect(
new BitVec(registry, '0x0100010500', true).toHuman()
).toEqual('0b00000001_00000000_00000001_00000101_00000000');
});
});
describe('toBoolArray() ordering', (): void => {
it('defaults to Lsb', (): void => {
expect(
new BitVec(registry, '0x0100010500').toBoolArray()
).toEqual([
true, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
true, false, false, false, false, false, false, false,
true, false, true, false, false, false, false, false,
false, false, false, false, false, false, false, false
]);
});
it('can output to Msb', (): void => {
expect(
new BitVec(registry, '0x0100010500', true).toBoolArray()
).toEqual([
false, false, false, false, false, false, false, true,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, true,
false, false, false, false, false, true, false, true,
false, false, false, false, false, false, false, false
]);
});
it('outputs all LSB bits', (): void => {
expect(
new BitVec(registry, '0x01000105ff').toBoolArray()
).toEqual([
true, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
true, false, false, false, false, false, false, false,
true, false, true, false, false, false, false, false,
true, true, true, true, true, true, true, true
]);
});
});
it('has a sane inspect', (): void => {
expect(
new BitVec(registry, '0x0837').inspect()
).toEqual({
// For input '0x0837' (yields 16 bits): compactToU8a(16) is 16<<2 = 64.
outer: [new Uint8Array([64]), new Uint8Array([0x08, 0x37])]
});
});
});
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyU8a, Inspect, Registry } from '../types/index.js';
import { compactFromU8aLim, compactToU8a, isString, u8aConcatStrict, u8aToU8a } from '@pezkuwi/util';
import { Raw } from '../native/Raw.js';
/** @internal */
function decodeBitVecU8a (value?: Uint8Array): [number, Uint8Array] {
if (!value?.length) {
return [0, new Uint8Array()];
}
// handle all other Uint8Array inputs, these do have a length prefix which is the number of bits encoded
const [offset, length] = compactFromU8aLim(value);
const total = offset + Math.ceil(length / 8);
if (total > value.length) {
throw new Error(`BitVec: required length less than remainder, expected at least ${total}, found ${value.length}`);
}
return [length, value.subarray(offset, total)];
}
/** @internal */
function decodeBitVec (value?: AnyU8a): [number, Uint8Array] {
if (Array.isArray(value) || isString(value)) {
const u8a = u8aToU8a(value);
return [u8a.length * 8, u8a];
}
return decodeBitVecU8a(value);
}
/**
* @name BitVec
* @description
* A BitVec that represents an array of bits. The bits are however stored encoded. The difference between this
* and a normal Bytes would be that the length prefix indicates the number of bits encoded, not the bytes
*/
export class BitVec extends Raw {
readonly #decodedLength: number;
readonly #isMsb?: boolean;
// In lieu of having the Msb/Lsb identifiers passed through, we default to assuming
// we are dealing with Lsb, which is the default (as of writing) BitVec format used
// in the Pezkuwi code (this only affects the toHuman displays)
constructor (registry: Registry, value?: AnyU8a, isMsb = false) {
const [decodedLength, u8a] = decodeBitVec(value);
super(registry, u8a);
this.#decodedLength = decodedLength;
this.#isMsb = isMsb;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public override get encodedLength (): number {
return this.length + compactToU8a(this.#decodedLength).length;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (): Inspect {
return {
outer: [compactToU8a(this.#decodedLength), super.toU8a()]
};
}
/**
* @description Creates a boolean array of the bit values
*/
public toBoolArray (): boolean[] {
const map = [...this.toU8a(true)].map((v) => [
!!(v & 0b1000_0000),
!!(v & 0b0100_0000),
!!(v & 0b0010_0000),
!!(v & 0b0001_0000),
!!(v & 0b0000_1000),
!!(v & 0b0000_0100),
!!(v & 0b0000_0010),
!!(v & 0b0000_0001)
]);
const count = map.length;
const result = new Array<boolean>(8 * count);
for (let i = 0; i < count; i++) {
const off = i * 8;
const v = map[i];
for (let j = 0; j < 8; j++) {
result[off + j] = this.#isMsb
? v[j]
: v[7 - j];
}
}
return result;
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public override toHuman (): string {
return `0b${
[...this.toU8a(true)]
.map((d) => `00000000${d.toString(2)}`.slice(-8))
.map((s) => this.#isMsb ? s : s.split('').reverse().join(''))
.join('_')
}`;
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return 'BitVec';
}
/**
* @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 override toU8a (isBare?: boolean): Uint8Array {
const bitVec = super.toU8a(isBare);
return isBare
? bitVec
: u8aConcatStrict([compactToU8a(this.#decodedLength), bitVec]);
}
}
@@ -0,0 +1,75 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Bytes } from '@pezkuwi/types-codec';
describe('Bytes', (): void => {
const registry = new TypeRegistry();
const NUM = [0x3a, 0x63, 0x6f, 0x64, 0x65];
const U8A = new Uint8Array([0x14, ...NUM]);
const HEX = '0x3a636f6465';
describe('construction', (): void => {
it('decodes when input is string', (): void => {
expect(
new Bytes(registry, ':code').toU8a()
).toEqual(U8A);
});
it('decodes when hex is not length prefixed', (): void => {
expect(
new Bytes(registry, HEX).toU8a()
).toEqual(U8A);
});
it('decodes from UInt8Array', (): void => {
expect(
new Bytes(registry, U8A).toU8a()
).toEqual(U8A);
});
it('decodes from number[]', (): void => {
expect(
new Bytes(registry, NUM).toU8a()
).toEqual(U8A);
});
it('creates via storagedata (no prefix)', (): void => {
expect(
new Bytes(
registry,
registry.createType('StorageData', HEX)
).toU8a()
).toEqual(U8A);
});
it('encodes from itself', (): void => {
expect(
new Bytes(registry, new Bytes(registry, HEX)).toU8a()
).toEqual(U8A);
});
it('strips length with toU8a(true)', (): void => {
expect(
new Bytes(registry, HEX).toU8a(true)
).toEqual(U8A.subarray(1));
});
it('strips length with toHex', (): void => {
expect(
new Bytes(registry, HEX).toHex()
).toEqual(HEX);
});
});
it('has a sane inspect', (): void => {
expect(
new Bytes(registry, '0x12345678').inspect()
).toEqual({
outer: [new Uint8Array([4 << 2]), new Uint8Array([0x12, 0x34, 0x56, 0x78])]
});
});
});
@@ -0,0 +1,88 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyU8a, Inspect, Registry } from '../types/index.js';
import { compactAddLength, compactFromU8aLim, compactToU8a, isString, isU8a, u8aToU8a } from '@pezkuwi/util';
import { Raw } from '../native/Raw.js';
// Bytes are used for things like on-chain code, so it has a healthy limit
const MAX_LENGTH = 10 * 1024 * 1024;
/** @internal */
function decodeBytesU8a (value: Uint8Array): [Uint8Array, number] {
if (!value.length) {
return [new Uint8Array(), 0];
}
// handle all other Uint8Array inputs, these do have a length prefix
const [offset, length] = compactFromU8aLim(value);
const total = offset + length;
if (length > MAX_LENGTH) {
throw new Error(`Bytes length ${length.toString()} exceeds ${MAX_LENGTH}`);
} else if (total > value.length) {
throw new Error(`Bytes: required length less than remainder, expected at least ${total}, found ${value.length}`);
}
return [value.subarray(offset, total), total];
}
/**
* @name Bytes
* @description
* A Bytes wrapper for Vec<u8>. The significant difference between this and a normal Uint8Array
* is that this version allows for length-encoding. (i.e. it is a variable-item codec, the same
* as what is found in [[Text]] and [[Vec]])
*/
export class Bytes extends Raw {
constructor (registry: Registry, value?: AnyU8a) {
const [u8a, decodedLength] = isU8a(value) && !(value instanceof Raw)
? decodeBytesU8a(value)
: Array.isArray(value) || isString(value)
? [u8aToU8a(value), 0]
: [value, 0];
super(registry, u8a, decodedLength);
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public override get encodedLength (): number {
return this.length + compactToU8a(this.length).length;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (isBare?: boolean): Inspect {
const clength = compactToU8a(this.length);
return {
outer: isBare
? [super.toU8a()]
: this.length
? [clength, super.toU8a()]
: [clength]
};
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return 'Bytes';
}
/**
* @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 override toU8a (isBare?: boolean): Uint8Array {
return isBare
? super.toU8a(isBare)
: compactAddLength(this);
}
}
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { HashMap, Struct, Text, U32 } from '@pezkuwi/types-codec';
import { stringToU8a } from '@pezkuwi/util';
const registry = new TypeRegistry();
describe('HashMap', (): void => {
it('generates sane toRawTypes', (): void => {
expect(new (HashMap.with(Text, U32))(registry).toRawType()).toBe('HashMap<Text,u32>');
expect(new (HashMap.with(Text, Text))(registry).toRawType()).toBe('HashMap<Text,Text>');
expect(new (HashMap.with(Text, Struct.with({ a: U32, b: Text })))(registry).toRawType())
.toBe('HashMap<Text,{"a":"u32","b":"Text"}>');
});
it('has a sane inspect', (): void => {
expect(
new (HashMap.with(Text, Text))(registry, new Map([
[new Text(registry, '1'), new Text(registry, 'foo')],
[new Text(registry, '2'), new Text(registry, 'bar')]
])).inspect()
).toEqual({
inner: [
{ outer: [new Uint8Array([1 << 2]), stringToU8a('1')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('foo')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('2')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('bar')] }
],
outer: [new Uint8Array([2 << 2])]
});
});
});
@@ -0,0 +1,16 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec, CodecClass, Registry } from '../types/index.js';
import { CodecMap } from './Map.js';
export class HashMap<K extends Codec = Codec, V extends Codec = Codec> extends CodecMap<K, V> {
public static with<K extends Codec, V extends Codec> (keyType: CodecClass<K> | string, valType: CodecClass<V> | string): CodecClass<CodecMap<K, V>> {
return class extends HashMap<K, V> {
constructor (registry: Registry, value?: Uint8Array | string | Map<any, any>) {
super(registry, keyType, valType, value);
}
};
}
}
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Linkage } from '@pezkuwi/types-codec';
const registry = new TypeRegistry();
describe('Linkage', (): void => {
it('decodes with ValidatorPrefs', (): void => {
const LINKA = { next: '5GznmRvdi5htUJKnMSWJgJUzSJJXSvWuHRSEdyUbHJZDNcwU', previous: null };
const PREFS = { commission: '10.00%' };
// prefs sanity check
expect(
registry.createType(
'ValidatorPrefsWithCommission',
'0x0284d717'
).toHuman()
).toEqual(PREFS);
// linkage sanity checks
expect(
new Linkage(registry, 'AccountId', '0x0001da30b68f54f686f586ddb29de12b682dd8bd1404566fb8a8db5dec20aa5b6b36').toHuman()
).toEqual(LINKA);
expect(
registry.createType(
'Linkage<AccountId>',
'0x0001da30b68f54f686f586ddb29de12b682dd8bd1404566fb8a8db5dec20aa5b6b36'
).toHuman()
).toEqual(LINKA);
// actual check
expect(
registry.createType(
'(ValidatorPrefsWithCommission, Linkage<AccountId>)',
'0x0284d7170001da30b68f54f686f586ddb29de12b682dd8bd1404566fb8a8db5dec20aa5b6b36'
).toHuman()
).toEqual([PREFS, LINKA]);
});
});
@@ -0,0 +1,81 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { Codec, CodecClass, Registry } from '../types/index.js';
import { Option } from '../base/Option.js';
import { Tuple } from '../base/Tuple.js';
import { Vec } from '../base/Vec.js';
import { Struct } from '../native/Struct.js';
type TypeWithValues = [CodecClass, any[]];
const EMPTY = new Uint8Array();
/**
* @name Linkage
* @description The wrapper for the result from a LinkedMap
*/
export class Linkage<T extends Codec> extends Struct {
constructor (registry: Registry, Type: CodecClass | string, value?: unknown) {
super(registry, {
previous: Option.with(Type),
// eslint-disable-next-line sort-keys
next: Option.with(Type)
}, value as HexString);
}
public static withKey<O extends Codec> (Type: CodecClass | string): CodecClass<Linkage<O>> {
return class extends Linkage<O> {
constructor (registry: Registry, value?: unknown) {
super(registry, Type, value);
}
};
}
/**
* @description Returns the next item the Linkage is pointing to
*/
public get previous (): Option<T> {
return this.get('previous') as Option<T>;
}
/**
* @description Returns the previous item the Linkage is pointing to
*/
public get next (): Option<T> {
return this.get('next') as Option<T>;
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return `Linkage<${this.next.toRawType(true)}>`;
}
/**
* @description Custom toU8a which with bare mode does not return the linkage if empty
*/
public override toU8a (isBare?: boolean): Uint8Array {
// As part of a storage query (where these appear), in the case of empty, the values
// are NOT populated by the node - follow the same logic, leaving it empty
return this.isEmpty
? EMPTY
: super.toU8a(isBare);
}
}
/**
* @name LinkageResult
* @description A Linkage keys/Values tuple
*/
export class LinkageResult extends Tuple {
constructor (registry: Registry, [TypeKey, keys]: TypeWithValues, [TypeValue, values]: TypeWithValues) {
super(registry, {
Keys: Vec.with(TypeKey),
Values: Vec.with(TypeValue)
}, [keys, values]);
}
}
@@ -0,0 +1,123 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { CodecMap, Text, U32 } from '@pezkuwi/types-codec';
import { stringToU8a } from '@pezkuwi/util';
const registry = new TypeRegistry();
const mockU32TextMap = new Map<Text, U32>();
mockU32TextMap.set(new Text(registry, 'bazzing'), new U32(registry, 69));
const mockU32TextMapString = '{"bazzing":69}';
const mockU32TextMapObject = { bazzing: 69 };
const mockU32TextMapHexString = '0x041c62617a7a696e6745000000';
const mockU32TextMapUint8Array = Uint8Array.from([4, 28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]);
const mockU32U32Map = new Map<U32, U32>();
mockU32U32Map.set(new U32(registry, 1), new U32(registry, 2));
mockU32U32Map.set(new U32(registry, 23), new U32(registry, 24));
mockU32U32Map.set(new U32(registry, 28), new U32(registry, 30));
mockU32U32Map.set(new U32(registry, 45), new U32(registry, 80));
const mockU32U32MapString = '{"1":2,"23":24,"28":30,"45":80}';
const mockU32U32MapObject = { 1: 2, 23: 24, 28: 30, 45: 80 };
const mockU32U32MapHexString = '0x10043102000000083233180000000832381e00000008343550000000';
const mockU32U32MapUint8Array = Uint8Array.from([16, 4, 49, 2, 0, 0, 0, 8, 50, 51, 24, 0, 0, 0, 8, 50, 56, 30, 0, 0, 0, 8, 52, 53, 80, 0, 0, 0]);
describe('CodecMap', (): void => {
describe('decoding', (): void => {
const testDecode = (type: string, input: unknown, output: string): void =>
it(`can decode from ${type}`, (): void => {
const s = new CodecMap(registry, Text, U32, input as string);
expect(s.toString()).toBe(output);
});
testDecode('map', mockU32TextMap, mockU32TextMapString);
testDecode('hex', mockU32TextMapHexString, mockU32TextMapString);
testDecode('Uint8Array', mockU32TextMapUint8Array, mockU32TextMapString);
testDecode('map', mockU32U32Map, mockU32U32MapString);
testDecode('hex', mockU32U32MapHexString, mockU32U32MapString);
testDecode('Uint8Array', mockU32U32MapUint8Array, mockU32U32MapString);
});
describe('encoding', (): void => {
const testEncode = (to: CodecTo, expected: any): void =>
it(`can encode ${to}`, (): void => {
const s = new CodecMap(registry, Text, U32, mockU32TextMap, 'BTreeMap');
expect(s[to]()).toEqual(expected);
});
testEncode('toHex', mockU32TextMapHexString);
testEncode('toJSON', mockU32TextMapObject);
testEncode('toU8a', mockU32TextMapUint8Array);
testEncode('toString', mockU32TextMapString);
});
describe('encoding multiple values', (): void => {
const testEncode = (to: CodecTo, expected: any): void =>
it(`can encode ${to}`, (): void => {
const s = new CodecMap(registry, Text, U32, mockU32U32Map, 'BTreeMap');
expect(s[to]()).toEqual(expected);
});
testEncode('toHex', mockU32U32MapHexString);
testEncode('toJSON', mockU32U32MapObject);
testEncode('toU8a', mockU32U32MapUint8Array);
testEncode('toString', mockU32U32MapString);
});
describe('enocodedLength & initialU8aLength', (): void => {
it('correctly encodes/decodes empty', (): void => {
const none = new CodecMap(registry, Text, Text, new Map([]));
// only the length byte
expect(none.toHex()).toEqual('0x00');
expect(none.encodedLength).toEqual(1);
expect(
new CodecMap(registry, Text, Text, none.toHex()).initialU8aLength
).toEqual(none.encodedLength);
});
it('correctly encodes/decodes filled', (): void => {
const some = new CodecMap(registry, Text, Text, new Map([
[new Text(registry, '1'), new Text(registry, 'foo')],
[new Text(registry, '2'), new Text(registry, 'bar')]
]));
// length byte + 2 values, 2 << 2 with Text values
expect(some.toHex()).toEqual('0x0804310c666f6f04320c626172');
expect(some.encodedLength).toEqual(1 + ((1 + 1) * 2) + ((1 + 3) * 2));
expect(
new CodecMap(registry, Text, Text, some.toHex()).initialU8aLength
).toEqual(some.encodedLength);
});
});
it('has a sane inspect', (): void => {
expect(
new CodecMap(registry, Text, Text, new Map([
[new Text(registry, '1'), new Text(registry, 'foo')],
[new Text(registry, '2'), new Text(registry, 'bar')]
])).inspect()
).toEqual({
inner: [
{ outer: [new Uint8Array([1 << 2]), stringToU8a('1')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('foo')] },
{ outer: [new Uint8Array([1 << 2]), stringToU8a('2')] },
{ outer: [new Uint8Array([3 << 2]), stringToU8a('bar')] }
],
outer: [new Uint8Array([2 << 2])]
});
});
});
+255
View File
@@ -0,0 +1,255 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, CodecClass, IMap, Inspect, IU8a, Registry } from '../types/index.js';
import { compactFromU8aLim, compactToU8a, isHex, isObject, isU8a, logger, stringify, u8aConcatStrict, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { AbstractArray } from '../abstract/Array.js';
import { Enum } from '../base/Enum.js';
import { Raw } from '../native/Raw.js';
import { Struct } from '../native/Struct.js';
import { compareMap, decodeU8a, sortMap, typeToConstructor } from '../utils/index.js';
const l = logger('Map');
/** @internal */
function decodeMapFromU8a<K extends Codec, V extends Codec> (registry: Registry, KeyClass: CodecClass<K>, ValClass: CodecClass<V>, u8a: Uint8Array): [CodecClass<K>, CodecClass<V>, Map<K, V>, number] {
const output = new Map<K, V>();
const [offset, count] = compactFromU8aLim(u8a);
const types = [];
for (let i = 0; i < count; i++) {
types.push(KeyClass, ValClass);
}
const [values, decodedLength] = decodeU8a(registry, new Array(types.length), u8a.subarray(offset), [types, []]);
for (let i = 0, count = values.length; i < count; i += 2) {
output.set(values[i] as K, values[i + 1] as V);
}
return [KeyClass, ValClass, output, offset + decodedLength];
}
/** @internal */
function decodeMapFromMap<K extends Codec, V extends Codec> (registry: Registry, KeyClass: CodecClass<K>, ValClass: CodecClass<V>, value: Map<any, any>): [CodecClass<K>, CodecClass<V>, Map<K, V>, number] {
const output = new Map<K, V>();
for (const [key, val] of value.entries()) {
const isComplex = KeyClass.prototype instanceof AbstractArray ||
KeyClass.prototype instanceof Struct ||
KeyClass.prototype instanceof Enum;
try {
output.set(
key instanceof KeyClass
? key
: new KeyClass(registry, isComplex && typeof key === 'string' ? JSON.parse(key) : 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 [KeyClass, ValClass, output, 0];
}
/**
* 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<any, any>, where both key and value types are either
* constructors or decodeable values for their types.
* @param jsonMap
* @internal
*/
function decodeMap<K extends Codec, V extends Codec> (registry: Registry, keyType: CodecClass<K> | string, valType: CodecClass<V> | string, value?: Uint8Array | string | Map<any, any>): [CodecClass<K>, CodecClass<V>, Map<K, V>, number] {
const KeyClass = typeToConstructor(registry, keyType);
const ValClass = typeToConstructor(registry, valType);
if (!value) {
return [KeyClass, ValClass, new Map<K, V>(), 0];
} else if (isU8a(value) || isHex(value)) {
return decodeMapFromU8a<K, V>(registry, KeyClass, ValClass, u8aToU8a(value));
} else if (value instanceof Map) {
return decodeMapFromMap<K, V>(registry, KeyClass, ValClass, value);
} else if (isObject(value)) {
return decodeMapFromMap<K, V>(registry, KeyClass, ValClass, new Map(Object.entries(value)));
}
throw new Error('Map: cannot decode type');
}
export class CodecMap<K extends Codec = Codec, V extends Codec = Codec> extends Map<K, V> implements IMap<K, V> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #KeyClass: CodecClass<K>;
readonly #ValClass: CodecClass<V>;
readonly #type: string;
constructor (registry: Registry, keyType: CodecClass<K> | string, valType: CodecClass<V> | string, rawValue: Uint8Array | string | Map<any, any> | undefined, type: 'BTreeMap' | 'HashMap' = 'HashMap') {
const [KeyClass, ValClass, decoded, decodedLength] = decodeMap(registry, keyType, valType, rawValue);
super(type === 'BTreeMap' ? sortMap(decoded) : decoded);
this.registry = registry;
this.initialU8aLength = decodedLength;
this.#KeyClass = KeyClass;
this.#ValClass = ValClass;
this.#type = type;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
let len = compactToU8a(this.size).length;
for (const [k, v] of this.entries()) {
len += k.encodedLength + v.encodedLength;
}
return len;
}
/**
* @description Returns a hash of the value
*/
public get hash (): IU8a {
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 breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
const inner: Inspect[] = [];
for (const [k, v] of this.entries()) {
inner.push(k.inspect());
inner.push(v.inspect());
}
return {
inner,
outer: [compactToU8a(this.size)]
};
}
/**
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
*/
public toHex (): HexString {
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, disableAscii?: boolean): Record<string, AnyJson> {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
json[
k instanceof Raw && !disableAscii && k.isAscii
? k.toUtf8()
: k.toString()
] = v.toHuman(isExtended, disableAscii);
}
return json;
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): Record<string, AnyJson> {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
json[k.toString()] = v.toJSON();
}
return json;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
json[
k instanceof Raw && !disableAscii && k.isAscii
? k.toUtf8()
: k.toString()
] = v.toPrimitive(disableAscii);
}
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 override toString (): string {
return 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: Uint8Array[] = [];
if (!isBare) {
encoded.push(compactToU8a(this.size));
}
for (const [k, v] of this.entries()) {
encoded.push(k.toU8a(isBare), v.toU8a(isBare));
}
return u8aConcatStrict(encoded);
}
}
@@ -0,0 +1,49 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { OptionBool } from '@pezkuwi/types-codec';
describe('OptionBool', (): void => {
const registry = new TypeRegistry();
describe('decodes', (): void => {
it('decodes none', (): void => {
expect(new OptionBool(registry).toJSON()).toEqual(null);
});
it('decodes true', (): void => {
expect(new OptionBool(registry, true).toJSON()).toEqual(true);
});
it('decodes false', (): void => {
expect(new OptionBool(registry, false).toJSON()).toEqual(false);
});
});
describe('encodes', (): void => {
it('encodes none', (): void => {
expect(new OptionBool(registry).toU8a()).toEqual(new Uint8Array([0]));
});
it('encodes true', (): void => {
expect(new OptionBool(registry, true).toU8a()).toEqual(new Uint8Array([1]));
});
it('encodes false', (): void => {
expect(new OptionBool(registry, false).toU8a()).toEqual(new Uint8Array([2]));
});
});
it('has a sane toRawType representation', (): void => {
expect(new OptionBool(registry).toRawType()).toEqual('Option<bool>');
});
it('has a sane inspect', (): void => {
expect(new OptionBool(registry, true).inspect()).toEqual({
outer: [new Uint8Array([1])]
});
});
});
@@ -0,0 +1,93 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyBool, Inspect, Registry } from '../types/index.js';
import { isHex, isU8a, u8aToU8a } from '@pezkuwi/util';
import { Option } from '../base/Option.js';
import { bool as Bool } from '../native/Bool.js';
function decodeU8a (registry: Registry, value: Uint8Array): null | Bool {
// Encoded as -
// - 0 = None
// - 1 = True
// - 2 = False
return value[0] === 0
? null
: new Bool(registry, value[0] === 1);
}
/**
* @name OptionBool
* @description A specific implementation of Option<bool> than allows for single-byte encoding
*/
export class OptionBool extends Option<Bool> {
constructor (registry: Registry, value?: Option<Bool> | AnyBool | Uint8Array | HexString | null) {
super(
registry,
Bool,
isU8a(value) || isHex(value)
? decodeU8a(registry, u8aToU8a(value))
: value
);
this.initialU8aLength = 1;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public override get encodedLength (): number {
return 1 | 0;
}
/**
* @description Checks if the value is an empty value (always false)
*/
public get isFalse (): boolean {
return this.isSome
? !this.value.valueOf()
: false;
}
/**
* @description Checks if the value is an empty value (always false)
*/
public get isTrue (): boolean {
return this.isSome
? this.value.valueOf()
: false;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (): Inspect {
return { outer: [this.toU8a()] };
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (isBare?: boolean): string {
return isBare
? 'bool'
: 'Option<bool>';
}
/**
* @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 override toU8a (isBare?: boolean): Uint8Array {
if (isBare) {
return super.toU8a(true);
}
return this.isSome
? new Uint8Array([this.isTrue ? 1 : 2])
: new Uint8Array([0]);
}
}
@@ -0,0 +1,37 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Range, U32 } from '@pezkuwi/types-codec';
describe('Range', (): void => {
const registry = new TypeRegistry();
let range: Range<U32>;
beforeEach((): void => {
range = new (Range.with(U32))(registry, [1, 2]);
});
it('decodes', (): void => {
expect(range.toJSON()).toEqual([1, 2]);
});
it('encodes', (): void => {
expect(range.toU8a()).toEqual(new Uint8Array([1, 0, 0, 0, 2, 0, 0, 0]));
});
it('has a sane toRawType representation', (): void => {
expect(range.toRawType()).toEqual('Range<u32>');
});
it('has a sane inspect', (): void => {
expect(range.inspect()).toEqual({
inner: [
{ outer: [new Uint8Array([1, 0, 0, 0])] },
{ outer: [new Uint8Array([2, 0, 0, 0])] }
]
});
});
});
@@ -0,0 +1,56 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyTuple, CodecClass, INumber, Registry } from '../types/index.js';
import { Tuple } from '../base/Tuple.js';
type RangeType = 'Range' | 'RangeInclusive';
interface Options {
rangeName?: RangeType;
}
/**
* @name Range
* @description
* Rust `Range<T>` representation
*/
export class Range<T extends INumber> extends Tuple {
#rangeName: RangeType;
constructor (registry: Registry, Type: CodecClass<T> | string, value?: AnyTuple, { rangeName = 'Range' }: Options = {}) {
super(registry, [Type, Type], value);
this.#rangeName = rangeName;
}
public static override with <T extends INumber> (Type: CodecClass<T> | string): CodecClass<Range<T>> {
return class extends Range<T> {
constructor (registry: Registry, value?: AnyTuple) {
super(registry, Type, value);
}
};
}
/**
* @description Returns the starting range value
*/
public get start (): T {
return this[0] as T;
}
/**
* @description Returns the ending range value
*/
public get end (): T {
return this[1] as T;
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return `${this.#rangeName}<${this.start.toRawType()}>`;
}
}
@@ -0,0 +1,20 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyTuple, CodecClass, INumber, Registry } from '../types/index.js';
import { Range } from './Range.js';
export class RangeInclusive<T extends INumber = INumber> extends Range<T> {
constructor (registry: Registry, Type: CodecClass<T> | string, value?: AnyTuple) {
super(registry, Type, value, { rangeName: 'RangeInclusive' });
}
public static override with <T extends INumber> (Type: CodecClass<T> | string): CodecClass<RangeInclusive<T>> {
return class extends RangeInclusive<T> {
constructor (registry: Registry, value?: AnyTuple) {
super(registry, Type, value);
}
};
}
}
@@ -0,0 +1,118 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Text, Type } from '@pezkuwi/types-codec';
import { stringToU8a, u8aConcat } from '@pezkuwi/util';
describe('Type', (): void => {
const registry = new TypeRegistry();
it('fails to cleanup invalid boxes', (): void => {
expect(
(): Type => new Type(registry, 'Box<Proposal')
).toThrow(/find closing matching/);
});
it('cleans up tuples with a single value', (): void => {
expect(
new Type(registry, '(AccountId)').toString()
).toEqual('AccountId');
});
it('does not touch tuples with multiple values', (): void => {
expect(
new Type(registry, '(AccountId, Balance)').toString()
).toEqual('(AccountId,Balance)');
});
it('handles nested types', (): void => {
expect(
new Type(registry, 'Box<Vec<AccountId>>').toString()
).toEqual('Vec<AccountId>');
});
it('handles nested types (embedded)', (): void => {
expect(
new Type(registry, '(u32, Box<Vec<AccountId>>)').toString()
).toEqual('(u32,Vec<AccountId>)');
});
it('handles aliasses, multiples per line', (): void => {
expect(
new Type(registry, '(Vec<u8>, AccountId, Vec<u8>)').toString()
).toEqual('(Bytes,AccountId,Bytes)');
});
it('removes whitespaces', (): void => {
expect(
new Type(registry, 'T :: AccountId').toString()
).toEqual('AccountId');
});
it('changes PairOf<T> -> (T, T)', (): void => {
expect(
new Type(registry, 'PairOf<T::Balance>').toString()
).toEqual('(Balance,Balance)');
});
it('changes PairOf<T> (embedded) -> (T, T)', (): void => {
expect(
new Type(registry, '(Vec<u8>, PairOf<T::Balance>, Vec<AccountId>)').toString()
).toEqual('(Bytes,(Balance,Balance),Vec<AccountId>)');
});
it('changes () -> ()', (): void => {
expect(
new Type(registry, '()').toString()
).toEqual('()');
});
it('has the sanitized', (): void => {
expect(
new Type(
registry,
new Text(registry, ' Box<Proposal> ')
).toString()
).toEqual('Proposal'); // eslint-disable-line
});
it('unwraps compact', (): void => {
expect(
new Type(registry, '<T::Balance as HasCompact>::Type').toString()
).toEqual('Compact<Balance>');
});
it('handles InherentOfflineReport', (): void => {
expect(
new Type(registry, '<T::InherentOfflineReport as InherentOfflineReport>::Inherent').toString()
).toEqual('InherentOfflineReport');
});
it('encodes correctly via toU8a()', (): void => {
const type = 'Compact<Balance>';
expect(new Text(registry, type).toU8a()).toEqual(
u8aConcat(
new Uint8Array([type.length << 2]),
stringToU8a(type)
)
);
});
it('creates a decodable U8a for sanitized types', (): void => {
const original = '<T::InherentOfflineReport as InherentOfflineReport>::Inherent';
const expected = 'InherentOfflineReport';
const u8a = new Type(registry, original).toU8a();
const decoded = new Type(registry, u8a);
expect(decoded.encodedLength).toEqual(original.length + 1); // extra byte for length
expect(decoded.toString()).toEqual(expected);
});
it('has the correct raw', (): void => {
expect(new Type(registry).toRawType()).toEqual('Type');
});
});
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registry } from '../types/index.js';
import { Text } from '../native/Text.js';
import { sanitize } from '../utils/index.js';
/**
* @name Type
* @description
* This is a extended version of Text, specifically to handle types. Here we rely fully
* on what Text provides us, however we also adjust the types received from the runtime,
* i.e. we remove the `T::` prefixes found in some types for consistency across implementation.
*/
export class Type extends Text {
constructor (registry: Registry, value: Text | Uint8Array | string = '') {
super(registry, value);
this.setOverride(sanitize(this.toString()));
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return 'Type';
}
}
@@ -0,0 +1,117 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { U8aFixed } from '@pezkuwi/types-codec';
describe('U8aFixed', (): void => {
const registry = new TypeRegistry();
describe('construction', (): void => {
it('allows empty values', (): void => {
expect(
new U8aFixed(registry).toHex()
).toEqual('0x0000000000000000000000000000000000000000000000000000000000000000');
});
it('allows construction via with', (): void => {
expect(
new (U8aFixed.with(64))(registry).bitLength()
).toEqual(64);
});
it('constructs from hex', (): void => {
expect(
new (U8aFixed.with(32))(registry, '0x01020304').toU8a()
).toEqual(
new Uint8Array([0x01, 0x02, 0x03, 0x04])
);
});
it('constructs from number[]', (): void => {
expect(
new (U8aFixed.with(32))(registry, [0x02, 0x03, 0x00, 0x00]).toU8a()
).toEqual(
new Uint8Array([0x02, 0x03, 0x00, 0x00])
);
});
it('constructs when passed Uint8Array is >= length', (): void => {
expect(
new (U8aFixed.with(32))(registry, new Uint8Array([0x00, 0x01, 0x02, 0x03])).toU8a()
).toEqual(
new Uint8Array([0x00, 0x01, 0x02, 0x03])
);
expect(
new (U8aFixed.with(32))(registry, new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05])).toU8a()
).toEqual(
new Uint8Array([0x00, 0x01, 0x02, 0x03])
);
});
it('constructs when passed string is === length', (): void => {
expect(
new (U8aFixed.with(32))(registry, '1234').toU8a()
).toEqual(
new Uint8Array([49, 50, 51, 52])
);
});
it('fails construction when passed string is > length', (): void => {
expect(
() => new (U8aFixed.with(32))(registry, '0x000102030405').toU8a()
).toThrow(/Expected input with 4 bytes/);
expect(
() => new (U8aFixed.with(256))(registry, '1363HWTPzDrzAQ6ChFiMU6mP4b6jmQid2ae55JQcKtZnpLGv')
).toThrow(/Expected input with 32 bytes/);
});
});
describe('utils', (): void => {
let u8a: U8aFixed;
beforeEach((): void => {
u8a = new U8aFixed(registry, [1, 2, 3, 4], 32);
});
it('limits the length', (): void => {
expect(u8a.length).toEqual(4);
});
it('exposes the correct bitLength', (): void => {
expect(u8a.bitLength()).toEqual(32);
});
it('allows wrapping of a pre-existing instance', (): void => {
expect(
u8a.toU8a()
).toEqual(new Uint8Array([1, 2, 3, 4]));
});
it('has a sane toRawType', (): void => {
expect(u8a.toRawType()).toEqual('[u8;4]');
});
it('has a sane inspect', (): void => {
expect(u8a.inspect()).toEqual({
outer: [new Uint8Array([1, 2, 3, 4])]
});
});
});
describe('static with', (): void => {
it('allows default toRawType', (): void => {
expect(
new (U8aFixed.with(64))(registry).toRawType()
).toEqual('[u8;8]');
});
it('allows toRawType override', (): void => {
expect(
new (U8aFixed.with(64, 'SomethingElse'))(registry).toRawType()
).toEqual('SomethingElse');
});
});
});
@@ -0,0 +1,57 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyU8a, CodecClass, Registry, U8aBitLength } from '../types/index.js';
import { isU8a, u8aToU8a } from '@pezkuwi/util';
import { Raw } from '../native/Raw.js';
/** @internal */
function decodeU8aFixed (value: AnyU8a, bitLength: U8aBitLength): [AnyU8a, number] {
const u8a = u8aToU8a(value);
const byteLength = bitLength / 8;
if (!u8a.length) {
return [new Uint8Array(byteLength), 0];
}
if (isU8a(value) ? u8a.length < byteLength : u8a.length !== byteLength) {
throw new Error(`Expected input with ${byteLength} bytes (${bitLength} bits), found ${u8a.length} bytes`);
}
return [u8a.subarray(0, byteLength), byteLength];
}
/**
* @name U8aFixed
* @description
* A U8a that manages a a sequence of bytes up to the specified bitLength. Not meant
* to be used directly, rather is should be subclassed with the specific lengths.
*/
export class U8aFixed extends Raw {
constructor (registry: Registry, value: AnyU8a = new Uint8Array(), bitLength: U8aBitLength = 256) {
const [u8a, decodedLength] = decodeU8aFixed(value, bitLength);
super(registry, u8a, decodedLength);
}
public static with (bitLength: U8aBitLength, typeName?: string): CodecClass<U8aFixed> {
return class extends U8aFixed {
constructor (registry: Registry, value?: AnyU8a) {
super(registry, value, bitLength);
}
public override toRawType (): string {
return typeName || super.toRawType();
}
};
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return `[u8;${this.length}]`;
}
}
@@ -0,0 +1,101 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { Bytes, Raw, u32, WrapperKeepOpaque } from '@pezkuwi/types-codec';
describe('WrapperKeepOpaque', (): void => {
const registry = new TypeRegistry();
const u8au32 = new Uint8Array([4 << 2, 135, 214, 18, 0]);
const ClazzUSize = WrapperKeepOpaque.with('usize');
const ClazzU32 = WrapperKeepOpaque.with(u32);
it('has handles non-decodable values', (): void => {
const usize = new ClazzUSize(registry, u8au32);
expect(usize.isDecoded).toEqual(false);
expect(() => usize.unwrap()).toThrow(/undecodable value/);
expect(usize.toHex()).toEqual('0x87d61200');
expect(usize.toHuman()).toEqual('0x87d61200');
expect(usize.toJSON()).toEqual('0x87d61200');
expect(usize.toRawType()).toEqual('WrapperKeepOpaque<usize>');
expect(usize.toString()).toEqual('0x87d61200');
expect(usize.toU8a()).toEqual(u8au32);
});
it('has handles decodable values', (): void => {
const u32 = new ClazzU32(registry, u8au32);
expect(u32.isDecoded).toEqual(true);
expect(u32.unwrap().toNumber()).toEqual(1234567);
expect(u32.toHex()).toEqual('0x87d61200');
expect(u32.toHuman()).toEqual('1,234,567');
expect(u32.toJSON()).toEqual('0x87d61200');
expect(u32.toRawType()).toEqual('WrapperKeepOpaque<u32>');
expect(u32.toString()).toEqual('1234567');
expect(u32.toU8a()).toEqual(u8au32);
});
it('handles values from Raw', (): void => {
const u32 = new ClazzU32(registry, new Raw(registry, u8au32.slice(1)));
expect(u32.unwrap().toNumber()).toEqual(1234567);
});
it('handles values from Bytes', (): void => {
const u32 = new ClazzU32(registry, new Bytes(registry, u8au32));
expect(u32.unwrap().toNumber()).toEqual(1234567);
});
it('has a sane inspect (non-decodable)', (): void => {
expect(
new ClazzUSize(registry, u8au32).inspect()
).toEqual({
outer: [new Uint8Array([4 << 2]), new Uint8Array([0x87, 0xd6, 0x12, 0x00])]
});
});
it('has a sane inspect (decodable)', (): void => {
expect(
new ClazzU32(registry, u8au32).inspect()
).toEqual({
inner: [{ outer: [new Uint8Array([0x87, 0xd6, 0x12, 0x00])] }],
outer: [new Uint8Array([4 << 2])]
});
});
it('has a sane in-wrapper representation', (): void => {
const set = registry.createType(
'BTreeSet<OpaquePeerId>',
// prefix
'0x' +
// 4 items, 16 >> 2
'10' +
// opaque length
'9c' +
// bytes length
'98' + '0024080112201ce5f00ef6e89374afb625f1ae4c1546d31234e87e3c3f51a62b91dd6bfa57df' +
// repeat the same for the next 3...
'9c98002408011220876a7b4984f98006dc8d666e28b60de307309835d775e7755cc770328cdacf2e9c98002408011220c81bc1d7057a1511eb9496f056f6f53cdfe0e14c8bd5ffca47c70a8d76c1326d9c98002408011220dacde7714d8551f674b8bb4b54239383c76a2b286fa436e93b2b7eb226bf4de7'
);
const val = [...set.values()];
expect(val.map((v) => v.toHex())).toEqual([
'0x980024080112201ce5f00ef6e89374afb625f1ae4c1546d31234e87e3c3f51a62b91dd6bfa57df',
'0x98002408011220876a7b4984f98006dc8d666e28b60de307309835d775e7755cc770328cdacf2e',
'0x98002408011220c81bc1d7057a1511eb9496f056f6f53cdfe0e14c8bd5ffca47c70a8d76c1326d',
'0x98002408011220dacde7714d8551f674b8bb4b54239383c76a2b286fa436e93b2b7eb226bf4de7'
]);
expect(val.map((v) => v.toHuman())).toEqual([
'0x0024080112201ce5f00ef6e89374afb625f1ae4c1546d31234e87e3c3f51a62b91dd6bfa57df',
'0x002408011220876a7b4984f98006dc8d666e28b60de307309835d775e7755cc770328cdacf2e',
'0x002408011220c81bc1d7057a1511eb9496f056f6f53cdfe0e14c8bd5ffca47c70a8d76c1326d',
'0x002408011220dacde7714d8551f674b8bb4b54239383c76a2b286fa436e93b2b7eb226bf4de7'
]);
});
});
@@ -0,0 +1,128 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyJson, AnyU8a, Codec, CodecClass, Inspect, Registry } from '../types/index.js';
import { compactAddLength, compactStripLength, compactToU8a, isHex, isU8a, u8aToU8a } from '@pezkuwi/util';
import { Raw } from '../native/Raw.js';
import { typeToConstructor } from '../utils/index.js';
import { Bytes } from './Bytes.js';
type OpaqueName = 'WrapperKeepOpaque' | 'WrapperOpaque';
interface Options {
opaqueName?: OpaqueName;
}
function decodeRaw<T extends Codec> (registry: Registry, typeName: CodecClass<T> | string, value?: unknown): [CodecClass<T>, T | null, AnyU8a] {
const Type = typeToConstructor(registry, typeName);
if (isU8a(value) || isHex(value)) {
try {
const [, u8a] = isHex(value)
? [0, u8aToU8a(value)]
: (value instanceof Raw)
? [0, value.subarray()]
: compactStripLength(value);
return [Type, new Type(registry, u8a), value];
} catch {
return [Type, null, value];
}
}
const instance = new Type(registry, value);
return [Type, instance, compactAddLength(instance.toU8a())];
}
export class WrapperKeepOpaque<T extends Codec> extends Bytes {
readonly #Type: CodecClass<T>;
readonly #decoded: T | null;
readonly #opaqueName: OpaqueName;
constructor (registry: Registry, typeName: CodecClass<T> | string, value?: unknown, { opaqueName = 'WrapperKeepOpaque' }: Options = {}) {
const [Type, decoded, u8a] = decodeRaw(registry, typeName, value);
super(registry, u8a);
this.#Type = Type;
this.#decoded = decoded;
this.#opaqueName = opaqueName;
}
public static with<T extends Codec> (Type: CodecClass<T> | string): CodecClass<WrapperKeepOpaque<T>> {
return class extends WrapperKeepOpaque<T> {
constructor (registry: Registry, value?: AnyU8a | T) {
super(registry, Type, value);
}
};
}
/**
* @description Checks if the wrapper is decodable
*/
public get isDecoded (): boolean {
return !!this.#decoded;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public override inspect (): Inspect {
return this.#decoded
? {
inner: [this.#decoded.inspect()],
outer: [compactToU8a(this.length)]
}
: {
outer: [compactToU8a(this.length), this.toU8a(true)]
};
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public override toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson {
return this.#decoded
? this.#decoded.toHuman(isExtended, disableAscii)
: super.toHuman(isExtended, disableAscii);
}
/**
* @description Converts the value in a best-fit primitive form
*/
public override toPrimitive (disableAscii?: boolean): any {
return this.#decoded
? this.#decoded.toPrimitive(disableAscii)
: super.toPrimitive(disableAscii);
}
/**
* @description Returns the base runtime type name for this instance
*/
public override toRawType (): string {
return `${this.#opaqueName}<${this.registry.getClassName(this.#Type) || (this.#decoded ? this.#decoded.toRawType() : new this.#Type(this.registry).toRawType())}>`;
}
/**
* @description Converts the Object to to a string (either decoded or bytes)
*/
public override toString (): string {
return this.#decoded
? this.#decoded.toString()
: super.toString();
}
/**
* @description Returns the decoded that the WrapperKeepOpaque represents (if available), throws if non-decodable
*/
public unwrap (): T {
if (!this.#decoded) {
throw new Error(`${this.#opaqueName}: unwrapping an undecodable value`);
}
return this.#decoded;
}
}
@@ -0,0 +1,58 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { WrapperOpaque } from '@pezkuwi/types-codec';
import { u8aConcat } from '@pezkuwi/util';
describe('WrapperOpaque', (): void => {
const registry = new TypeRegistry();
const u8au32 = new Uint8Array([4 << 2, 135, 214, 18, 0]);
it('u8a encodes a wrapped u32 correctly', (): void => {
expect(
new WrapperOpaque(registry, 'u32', 1234567).toU8a()
).toEqual(u8au32);
});
it('u8a decodes a wrapped u32 correctly', (): void => {
expect(
new WrapperOpaque(registry, 'u32', u8au32).toU8a()
).toEqual(u8au32);
});
it('u8a encodes a wrapped option correctly', (): void => {
expect(
new WrapperOpaque(registry, 'Option<u32>', 1234567).toU8a()
).toEqual(u8aConcat([5 << 2, 1], u8au32.slice(1)));
});
it('hex encodes a wrapped u32 correctly', (): void => {
expect(
new WrapperOpaque(registry, 'u32', '0x12345678').toHex()
).toEqual('0x12345678');
});
it('has the correct unwrap', (): void => {
expect(
new WrapperOpaque(registry, 'u32', '0x12345678').unwrap().toHex()
).toEqual('0x78563412');
});
it('has the correct toRawType', (): void => {
expect(
new WrapperOpaque(registry, 'u32').toRawType()
).toEqual('WrapperOpaque<u32>');
});
it('has a sane inspect', (): void => {
expect(
new WrapperOpaque(registry, 'u32', '0x78563412').inspect()
).toEqual({
inner: [{ outer: [new Uint8Array([0x78, 0x56, 0x34, 0x12])] }],
outer: [new Uint8Array([4 << 2])]
});
});
});
@@ -0,0 +1,27 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec, CodecClass, Registry } from '../types/index.js';
import { WrapperKeepOpaque } from './WrapperKeepOpaque.js';
export class WrapperOpaque<T extends Codec> extends WrapperKeepOpaque<T> {
constructor (registry: Registry, typeName: CodecClass<T> | string, value?: unknown) {
super(registry, typeName, value, { opaqueName: 'WrapperOpaque' });
}
public static override with<T extends Codec> (Type: CodecClass<T> | string): CodecClass<WrapperKeepOpaque<T>> {
return class extends WrapperOpaque<T> {
constructor (registry: Registry, value?: unknown) {
super(registry, Type, value);
}
};
}
/**
* @description The inner value for this wrapper, in all cases it _should_ be decodable (unlike KeepOpaque)
*/
public get inner (): T {
return this.unwrap();
}
}
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { BitVec } from './BitVec.js';
export { BTreeMap } from './BTreeMap.js';
export { BTreeSet } from './BTreeSet.js';
export { Bytes } from './Bytes.js';
export { HashMap } from './HashMap.js';
export { Linkage } from './Linkage.js';
export { CodecMap, CodecMap as Map } from './Map.js';
export { OptionBool } from './OptionBool.js';
export { Range } from './Range.js';
export { RangeInclusive } from './RangeInclusive.js';
export { Type } from './Type.js';
export { U8aFixed } from './U8aFixed.js';
export { WrapperKeepOpaque } from './WrapperKeepOpaque.js';
export { WrapperOpaque } from './WrapperOpaque.js';
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import './packageDetect.js';
export * from './bundle.js';
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';
@@ -0,0 +1,74 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { Bool } from '@pezkuwi/types-codec';
describe('Bool', (): void => {
const registry = new TypeRegistry();
describe('decode', (): void => {
// eslint-disable-next-line @typescript-eslint/ban-types
const testDecode = (type: string, input: Uint8Array | boolean | Boolean | Bool | number, expected: boolean): void =>
it(`can decode from ${type}`, (): void => {
expect(new Bool(registry, input).toJSON()).toBe(expected);
});
testDecode('Bool', new Bool(registry, true), true);
testDecode('Boolean', Boolean(true), true);
testDecode('boolean', true, true);
testDecode('number', 1, true);
testDecode('Uint8Array', Uint8Array.from([1]), true);
});
describe('encode', (): void => {
const testEncode = (to: CodecTo, expected: string | Uint8Array | boolean, value: boolean): void =>
it(`can encode ${to}`, (): void => {
expect(new Bool(registry, value)[to]()).toEqual(expected);
});
testEncode('toJSON', true, true);
testEncode('toHex', '0x01', true);
testEncode('toString', 'true', true);
testEncode('toU8a', Uint8Array.from([1]), true);
testEncode('toU8a', Uint8Array.from([0]), false);
});
it('correctly encodes length', (): void => {
expect(new Bool(registry, true).encodedLength).toEqual(1);
});
describe('utils', (): void => {
it('compares against a boolean', (): void => {
expect(new Bool(registry, true).eq(true)).toBe(true);
});
it('compares against a Bool', (): void => {
expect(new Bool(registry, false).eq(new Bool(registry, false))).toBe(true);
});
it('has isTrue', (): void => {
expect(new Bool(registry, true).isTrue).toBe(true);
});
it('has isFalse', (): void => {
expect(new Bool(registry, true).isFalse).toBe(false);
});
it('has sane isEmpty aligning with the rest', (): void => {
expect(new Bool(registry).isEmpty).toBe(true);
expect(new Bool(registry, false).isEmpty).toBe(true);
expect(new Bool(registry, true).isEmpty).toBe(false);
});
it('has a sane inspect', (): void => {
expect(new Bool(registry, true).inspect()).toEqual({
outer: [new Uint8Array([1])]
});
});
});
});
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyBool, Codec, Inspect, IU8a, Registry } from '../types/index.js';
import { isU8a, u8aToHex } from '@pezkuwi/util';
/**
* @name bool
* @description
* Representation for a boolean value in the system. It extends the base JS `Boolean` class
* @noInheritDoc
*/
export class bool extends Boolean implements Codec {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength = 1;
public isStorageFallback?: boolean;
constructor (registry: Registry, value: bool | AnyBool | Uint8Array | number = false) {
super(
isU8a(value)
? value[0] === 1
: value instanceof Boolean
? value.valueOf()
: !!value
);
this.registry = registry;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return 1 | 0;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value (true when it wraps false/default)
*/
public get isEmpty (): boolean {
return this.isFalse;
}
/**
* @description Checks if the value is an empty value (always false)
*/
public get isFalse (): boolean {
return !this.isTrue;
}
/**
* @description Checks if the value is an empty value (always false)
*/
public get isTrue (): boolean {
return this.valueOf();
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return this.valueOf() === (
other instanceof Boolean
? other.valueOf()
: other
);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): boolean {
return this.toJSON();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): boolean {
return this.valueOf();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): boolean {
return this.toJSON();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'bool';
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return this.toJSON().toString();
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return new Uint8Array([this.valueOf() ? 1 : 0]);
}
}
@@ -0,0 +1,85 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { CodecDate, U64 } from '@pezkuwi/types-codec';
import { BN } from '@pezkuwi/util';
describe('Date', (): void => {
const registry = new TypeRegistry();
describe('decode', (): void => {
const testDecode = (type: string, input: Date | CodecDate | U64 | number, expected: string | number, toJSON = false): void =>
it(`can decode from ${type}`, (): void => {
expect(new CodecDate(registry, input)[toJSON ? 'toJSON' : 'toISOString']()).toBe(expected);
});
testDecode('Date', new Date(1537968546280), '2018-09-26T13:29:06.280Z');
testDecode('CodecDate', new CodecDate(registry, 1234), 1234, true);
testDecode('number', 1234, 1234, true);
testDecode('U64', new U64(registry, 69), 69, true);
});
describe('encode', (): void => {
const testEncode = (to: 'toBigInt' | 'toBn' | 'toISOString' | 'toNumber' | CodecTo, expected: bigint | BN | number | string | Uint8Array): void =>
it(`can encode ${to}`, (): void => {
expect(new CodecDate(registry, 421)[to]()).toEqual(expected);
});
testEncode('toBigInt', 421n);
testEncode('toBn', new BN(421));
testEncode('toJSON', 421);
testEncode('toISOString', '1970-01-01T00:07:01.000Z');
testEncode('toNumber', 421);
testEncode('toU8a', Uint8Array.from([165, 1, 0, 0, 0, 0, 0, 0]));
it('can encode toString', (): void => {
const date = new Date(Date.UTC(1970, 0, 1, 2, 3, 4));
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
expect(
new CodecDate(registry, date).toString()
).toMatch(/^Thu Jan 01 1970 02:03:04/);
});
it('encodes default BE hex', (): void => {
expect(
new CodecDate(registry, 3).toHex()
).toEqual('0x0000000000000003');
});
it('encodes options LE hex', (): void => {
expect(
new CodecDate(registry, 3).toHex(true)
).toEqual('0x0300000000000000');
});
it('encodes correctly to BigInt', (): void => {
expect(
new CodecDate(registry, 41).toBigInt() + 1n
).toEqual(42n);
});
});
describe('utils', (): void => {
it('compares values', (): void => {
expect(new CodecDate(registry, 123).eq(123)).toBe(true);
});
it('compares values (non-match)', (): void => {
expect(new CodecDate(registry, 123).eq(456)).toBe(false);
});
it('has a sane inspect', (): void => {
expect(new CodecDate(registry, 3).inspect()).toEqual({
outer: [new Uint8Array([3, 0, 0, 0, 0, 0, 0, 0])]
});
});
});
});
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyNumber, Inspect, INumber, IU8a, Registry, UIntBitLength } from '../types/index.js';
import { BN, bnToBn, bnToHex, bnToU8a, isString, isU8a, u8aToBn } from '@pezkuwi/util';
const BITLENGTH: UIntBitLength = 64;
const U8A_OPTS = { bitLength: BITLENGTH, isLe: true };
function decodeDate (value: CodecDate | Date | AnyNumber): Date {
if (isU8a(value)) {
value = u8aToBn(value.subarray(0, BITLENGTH / 8));
} else if (value instanceof Date) {
return value;
} else if (isString(value)) {
value = new BN(value.toString(), 10, 'le');
}
return new Date(
bnToBn(value as BN).toNumber() * 1000
);
}
/**
* @name Date
* @description
* A wrapper around seconds/timestamps. Internally the representation only has
* second precicion (aligning with Rust), so any numbers passed an/out are always
* per-second. For any encoding/decoding the 1000 multiplier would be applied to
* get it in line with JavaScript formats. It extends the base JS `Date` object
* and has all the methods available that are applicable to any `Date`
* @noInheritDoc
*/
export class CodecDate extends Date implements INumber {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength = BITLENGTH / 8;
public isStorageFallback?: boolean;
constructor (registry: Registry, value: CodecDate | Date | AnyNumber = 0) {
super(decodeDate(value));
this.registry = registry;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return BITLENGTH / 8;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.getTime() === 0;
}
/**
* @description Returns the number of bits in the value
*/
public bitLength (): UIntBitLength {
return BITLENGTH;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return decodeDate(other as AnyNumber).getTime() === this.getTime();
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a BigInt representation of the number
*/
public toBigInt (): bigint {
return BigInt(this.toNumber());
}
/**
* @description Returns the BN representation of the timestamp
*/
public toBn (): BN {
return new BN(this.toNumber());
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (isLe = false): HexString {
return bnToHex(this.toBn(), {
bitLength: BITLENGTH,
isLe,
isNegative: false
});
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): string {
return this.toISOString();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public override toJSON (): any {
// FIXME Return type should be number, but conflicts with Date.toJSON()
// which returns string
return this.toNumber();
}
/**
* @description Returns the number representation for the timestamp
*/
public toNumber (): number {
return Math.ceil(this.getTime() / 1000);
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): number {
return this.toNumber();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'Moment';
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
// only included here since we do not inherit docs
return super.toString();
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return bnToU8a(this.toNumber(), U8A_OPTS);
}
}
@@ -0,0 +1,51 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { TypeRegistry } from '@pezkuwi/types';
import { F32, F64 } from '@pezkuwi/types-codec';
describe('Float', (): void => {
const registry = new TypeRegistry();
describe('F32', (): void => {
it('has a sane toRawType()', (): void => {
expect(
new F32(registry).toRawType()
).toEqual('f32');
});
it('constructs from Uint8Array', (): void => {
expect(
new F32(registry, new Uint8Array([0, 0, 0, 128])).toNumber()
).toEqual(-0.0);
});
it('triggers isEmpty on 0', (): void => {
expect(
new F32(registry, 0).isEmpty
).toEqual(true);
});
it('constructs from a float value', (): void => {
expect(
new F32(registry, 123.456).toString()
).toEqual('123.456');
});
});
describe('F64', (): void => {
it('has a sane toRawType()', (): void => {
expect(
new F64(registry).toRawType()
).toEqual('f64');
});
it('constructs from hex', (): void => {
expect(
new F64(registry, '0x0000000000000080').toNumber()
).toEqual(-0.0);
});
});
});
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyFloat, CodecClass, IFloat, Inspect, IU8a, Registry } from '../types/index.js';
import { floatToU8a, isHex, isU8a, u8aToFloat, u8aToHex, u8aToU8a } from '@pezkuwi/util';
interface Options {
bitLength?: 32 | 64;
}
/**
* @name Float
* @description
* A Codec wrapper for F32 & F64 values. You generally don't want to be using
* f32/f64 in your runtime, operations on fixed points numbers are preferable. This class
* was explicitly added since scale-codec has a flag that enables this and it is available
* in some eth_* RPCs
*/
export class Float extends Number implements IFloat {
readonly encodedLength: number;
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #bitLength: 32 | 64;
constructor (registry: Registry, value?: AnyFloat, { bitLength = 32 }: Options = {}) {
super(
isU8a(value) || isHex(value)
? value.length === 0
? 0
: u8aToFloat(u8aToU8a(value), { bitLength })
: (value || 0)
);
this.#bitLength = bitLength;
this.encodedLength = bitLength / 8;
this.initialU8aLength = this.encodedLength;
this.registry = registry;
}
public static with (bitLength: 32 | 64): CodecClass<Float> {
return class extends Float {
constructor (registry: Registry, value?: AnyFloat) {
super(registry, value, { bitLength });
}
};
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Returns true if the type wraps an empty/default all-0 value
*/
get isEmpty (): boolean {
return this.valueOf() === 0;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return this.valueOf() === Number(other);
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): string {
return this.toString();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): string {
// Not sure if this is actually a hex or a string value
// (would need to check against RPCs to see the result here)
return this.toHex();
}
/**
* @description Returns the number representation (Same as valueOf)
*/
public toNumber (): number {
return this.valueOf();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): number {
return this.toNumber();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return `f${this.#bitLength}`;
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return floatToU8a(this, {
bitLength: this.#bitLength
});
}
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, Codec, Inspect, IU8a, Registry } from '../types/index.js';
import { isFunction, objectProperties, stringify } from '@pezkuwi/util';
import { compareMap } from '../utils/index.js';
/** @internal */
function decodeJson (value?: Record<string, unknown> | null): [string, any][] {
return Object.entries(value || {});
}
/**
* @name Json
* @description
* Wraps the a JSON structure retrieve via RPC. It extends the standard JS Map with. While it
* implements a Codec, it is limited in that it can only be used with input objects via RPC,
* i.e. no hex decoding. Unlike a struct, this waps a JSON object with unknown keys
* @noInheritDoc
*/
export class Json extends Map<string, any> implements Codec {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
constructor (registry: Registry, value?: Record<string, unknown> | null) {
const decoded = decodeJson(value);
super(decoded);
this.registry = registry;
objectProperties(this, decoded.map(([k]) => k), (k) => this.get(k));
}
/**
* @description Always 0, never encodes as a Uint8Array
*/
public get encodedLength (): number {
return 0 | 0;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return [...this.keys()].length === 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 typed value from the internal map
*/
public getT <T> (key: string): T {
return this.get(key) as unknown as T;
}
/**
* @description Unimplemented, will throw
*/
public inspect (): Inspect {
throw new Error('Unimplemented');
}
/**
* @description Unimplemented, will throw
*/
public toHex (): HexString {
throw new Error('Unimplemented');
}
/**
* @description Converts the Object to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): Record<string, AnyJson> {
return [...this.entries()].reduce<Record<string, AnyJson>>((json, [key, value]): Record<string, AnyJson> => {
json[key] = isFunction((value as Codec)?.toHuman)
? (value as Codec).toHuman()
: value as AnyJson;
return json;
}, {});
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): Record<string, AnyJson> {
return [...this.entries()].reduce<Record<string, AnyJson>>((json, [key, value]): Record<string, AnyJson> => {
json[key] = value as AnyJson;
return json;
}, {});
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): Record<string, AnyJson> {
return [...this.entries()].reduce<Record<string, AnyJson>>((json, [key, value]): Record<string, AnyJson> => {
json[key] = isFunction((value as Codec).toPrimitive)
? (value as Codec).toPrimitive(disableAscii)
: value as AnyJson;
return json;
}, {});
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'Json';
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return stringify(this.toJSON());
}
/**
* @description Unimplemented, will throw
*/
public toU8a (_isBare?: boolean): Uint8Array {
throw new Error('Unimplemented');
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { AnyU8a, CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { Raw } from '@pezkuwi/types-codec';
const registry = new TypeRegistry();
const testDecode = (type: string, input: AnyU8a, expected: string): void =>
it(`can decode from ${type}`, (): void => {
const e = new Raw(registry, input);
expect(e.toString()).toBe(expected);
});
const testEncode = (to: CodecTo, expected: AnyU8a): void =>
it(`can encode ${to}`, (): void => {
const e = new Raw(registry, [1, 2, 3, 4, 5]);
expect(e[to]()).toEqual(expected);
});
describe('Raw', (): void => {
let u8a: Raw;
beforeEach((): void => {
u8a = new Raw(registry, [1, 2, 3, 4, 5]);
});
testDecode('Array', [1, 2, 3, 4, 5], '0x0102030405');
testDecode('hex', '0x0102030405', '0x0102030405');
testDecode('U8a', new Uint8Array([1, 2, 3, 4, 5]), '0x0102030405');
testDecode('Uint8Array', Uint8Array.from([1, 2, 3, 4, 5]), '0x0102030405');
testEncode('toJSON', '0x0102030405');
testEncode('toHex', '0x0102030405');
testEncode('toPrimitive', '0x0102030405');
testEncode('toString', '0x0102030405');
testEncode('toU8a', Uint8Array.from([1, 2, 3, 4, 5]));
it('contains the length of the elements', (): void => {
expect(u8a.length).toEqual(5);
});
it('correctly encodes length', (): void => {
expect(u8a.encodedLength).toEqual(5);
});
it('allows wrapping of a pre-existing instance', (): void => {
expect(
new Raw(registry, u8a).length
).toEqual(5);
});
it('implements subarray correctly', (): void => {
expect(u8a.subarray(1, 3)).toEqual(Uint8Array.from([2, 3]));
});
describe('utils', (): void => {
it('compares against other U8a', (): void => {
expect(u8a.eq(new Uint8Array([1, 2, 3, 4, 5]))).toBe(true);
});
it('compares against other U8a (non-length)', (): void => {
expect(u8a.eq(new Uint8Array([1, 2, 3, 4]))).toBe(false);
});
it('compares against other U8a (mismatch)', (): void => {
expect(u8a.eq(new Uint8Array([1, 2, 3, 4, 6]))).toBe(false);
});
it('compares against hex inputs', (): void => {
expect(u8a.eq('0x0102030405')).toBe(true);
});
it('has valid isAscii', (): void => {
expect(u8a.isAscii).toBe(false);
expect(new Raw(registry, '0x2021222324').isAscii).toBe(true);
});
it('has valid toHuman with disableAscii option set as true', (): void => {
expect(new Raw(registry, new Uint8Array([85, 85, 85, 85, 85, 85, 85, 85])).toHuman(undefined, true)).toEqual('0x5555555555555555');
});
it('has valid toPrimitive', (): void => {
expect(new Raw(registry, 'testing').toPrimitive()).toEqual('testing');
expect(new Raw(registry, '0xe4bda0e5a5bd').toPrimitive()).toEqual('0xe4bda0e5a5bd');
});
it('has valid toUtf8', (): void => {
expect(new Raw(registry, 'Приветствую, ми').toUtf8()).toEqual('Приветствую, ми');
expect(new Raw(registry, '0xe4bda0e5a5bd').toUtf8()).toEqual('你好');
});
it('throws on invalid utf8', (): void => {
expect(
() => new Raw(registry, '0x7f07b1f87709608bee603bbc79a0dfc29cd315c1351a83aa31adf7458d7d3003').toUtf8()
).toThrow(/The character sequence is not a valid Utf8 string/);
});
it('has a sane inspect', (): void => {
expect(
new Raw(registry, [1, 2, 3, 4, 5]).inspect()
).toEqual({
outer: [new Uint8Array([1, 2, 3, 4, 5])]
});
});
});
});
+171
View File
@@ -0,0 +1,171 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, AnyU8a, Inspect, IU8a, Registry } from '../types/index.js';
import { isAscii, isUndefined, isUtf8, u8aToHex, u8aToString, u8aToU8a } from '@pezkuwi/util';
/**
* @name Raw
* @description
* A basic wrapper around Uint8Array, with no frills and no fuss. It does differ
* from other implementations where it will consume the full Uint8Array as passed to it.
* As such it is meant to be subclassed where the wrapper takes care of the
* actual lengths instead of used directly.
* @noInheritDoc
*/
export class Raw extends Uint8Array implements IU8a {
readonly registry: Registry;
public createdAtHash?: IU8a | undefined;
public initialU8aLength?: number | undefined;
public isStorageFallback?: boolean;
/**
* @description This ensures that operators such as clice, filter, map, etc. return
* new Array instances (without this we need to apply overrides)
*/
static get [Symbol.species] (): typeof Uint8Array {
return Uint8Array;
}
constructor (registry: Registry, value?: AnyU8a, initialU8aLength?: number) {
super(u8aToU8a(value));
this.registry = registry;
this.initialU8aLength = initialU8aLength;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.length;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Returns true if the wrapped value contains only ASCII printable characters
*/
public get isAscii (): boolean {
return isAscii(this);
}
/**
* @description Returns true if the type wraps an empty/default all-0 value
*/
public get isEmpty (): boolean {
return !this.length || isUndefined(this.find((b) => !!b));
}
/**
* @description Returns true if the wrapped value contains only utf8 characters
*/
public get isUtf8 (): boolean {
return isUtf8(this);
}
/**
* @description Returns the number of bits in the value
*/
public bitLength (): number {
return this.length * 8;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
if (other instanceof Uint8Array) {
return (this.length === other.length) &&
!this.some((b, index) => b !== other[index]);
}
return this.eq(u8aToU8a(other as string));
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
return u8aToHex(this);
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (_isExtended?: boolean, disableAscii?: boolean): AnyJson {
return this.toPrimitive(disableAscii);
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): string {
return this.toHex();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): AnyJson {
if (!disableAscii && this.isAscii) {
const text = this.toUtf8();
// ensure we didn't end up with multibyte codepoints
if (isAscii(text)) {
return text;
}
}
return this.toJSON();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'Raw';
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return this.toHex();
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return Uint8Array.from(this);
}
/**
* @description Returns the wrapped data as a UTF-8 string
*/
public toUtf8 (): string {
if (!this.isUtf8) {
throw new Error('The character sequence is not a valid Utf8 string');
}
return u8aToString(this);
}
}
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
/* eslint-disable sort-keys */
import { TypeRegistry } from '@pezkuwi/types';
import { CodecSet } from '@pezkuwi/types-codec';
import { stringify } from '@pezkuwi/util';
// TODO actually import these from definitions, don't re-define here
const SET_FIELDS = {
header: 0b00000001,
body: 0b00000010,
receipt: 0b00000100,
messageQueue: 0b00001000,
justification: 0b00010000
};
const SET_ROLES = {
none: 0b00000000,
full: 0b00000001,
light: 0b00000010,
authority: 0b00000100
};
const SET_WITHDRAW = {
TransactionPayment: 0b00000001,
Transfer: 0b00000010,
Reserve: 0b00000100,
Fee: 0b00001000
};
describe('Set', (): void => {
const registry = new TypeRegistry();
it('constructs via an string[]', (): void => {
const set = new CodecSet(registry, SET_ROLES, ['full', 'authority']);
expect(set.isEmpty).toEqual(false);
expect(set.toString()).toEqual(
'[full, authority]'
);
});
it('throws with invalid values', (): void => {
expect(
() => new CodecSet(registry, SET_ROLES, ['full', 'authority', 'invalid'])
).toThrow(/Invalid key 'invalid'/);
});
it('throws with add on invalid', (): void => {
expect(
() => (new CodecSet(registry, SET_ROLES, [])).add('invalid')
).toThrow(/Invalid key 'invalid'/);
});
it('allows construction via number', (): void => {
expect(
(new CodecSet(registry, SET_WITHDRAW, 15)).eq(['TransactionPayment', 'Transfer', 'Reserve', 'Fee'])
).toBe(true);
});
it('does not allow invalid number', (): void => {
expect(
() => new CodecSet(registry, SET_WITHDRAW, 31)
).toThrow(/Mismatch decoding '31', computed as '15'/);
});
it('hash a valid encoding', (): void => {
const set = new CodecSet(registry, SET_FIELDS, ['header', 'body', 'justification']);
expect(set.toU8a()).toEqual(new Uint8Array([19]));
});
describe('utils', (): void => {
const set = new CodecSet(registry, SET_ROLES, ['full', 'authority']);
it('compares against string array', (): void => {
expect(
set.eq(['authority', 'full'])
).toBe(true);
});
it('compares against number (encoded)', (): void => {
expect(
set.eq(SET_ROLES.full | SET_ROLES.authority)
).toBe(true);
});
it('compares against other sets', (): void => {
expect(
set.eq(new CodecSet(registry, SET_ROLES, ['authority', 'full']))
).toBe(true);
});
it('returns false on other values', (): void => {
expect(
set.eq('full')
).toBe(false);
});
it('has a sane inspect', (): void => {
expect(set.inspect()).toEqual({
outer: [new Uint8Array([SET_ROLES.full | SET_ROLES.authority])]
});
});
});
it('has a sane toRawType representation', (): void => {
expect(
new CodecSet(registry, { a: 1, b: 2, c: 345 }).toRawType()
).toEqual(stringify({
_set: { a: 1, b: 2, c: 345 }
}));
});
});
+269
View File
@@ -0,0 +1,269 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { CodecClass, Inspect, ISet, IU8a, Registry } from '../types/index.js';
import { BN, bnToBn, bnToU8a, isBn, isNumber, isString, isU8a, isUndefined, objectProperties, stringify, stringPascalCase, u8aToBn, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { compareArray } from '../utils/index.js';
type SetValues = Record<string, number | BN>;
function encodeSet (setValues: SetValues, values: string[]): BN {
const encoded = new BN(0);
for (let i = 0, count = values.length; i < count; i++) {
encoded.ior(bnToBn(setValues[values[i]] || 0));
}
return encoded;
}
/** @internal */
function decodeSetArray (setValues: SetValues, values: string[]): string[] {
const count = values.length;
const result = new Array<string>(count);
for (let i = 0; i < count; i++) {
const key = values[i];
if (isUndefined(setValues[key])) {
throw new Error(`Set: Invalid key '${key}' passed to Set, allowed ${Object.keys(setValues).join(', ')}`);
}
result[i] = key;
}
return result;
}
/** @internal */
function decodeSetNumber (setValues: SetValues, _value: BN | number): string[] {
const bn = bnToBn(_value);
const keys = Object.keys(setValues);
const result: string[] = [];
for (let i = 0, count = keys.length; i < count; i++) {
const key = keys[i];
if (bn.and(bnToBn(setValues[key])).eq(bnToBn(setValues[key]))) {
result.push(key);
}
}
const computed = encodeSet(setValues, result);
if (!bn.eq(computed)) {
throw new Error(`Set: Mismatch decoding '${bn.toString()}', computed as '${computed.toString()}' with ${result.join(', ')}`);
}
return result;
}
/** @internal */
function decodeSet (setValues: SetValues, value: string[] | Set<string> | Uint8Array | BN | number | string = 0, bitLength: number): string[] {
if (bitLength % 8 !== 0) {
throw new Error(`Expected valid bitLength, power of 8, found ${bitLength}`);
}
const byteLength = bitLength / 8;
if (isU8a(value)) {
return value.length === 0
? []
: decodeSetNumber(setValues, u8aToBn(value.subarray(0, byteLength), { isLe: true }));
} else if (isString(value)) {
return decodeSet(setValues, u8aToU8a(value), byteLength);
} else if (value instanceof Set || Array.isArray(value)) {
const input = Array.isArray(value)
? value
: [...value.values()];
return decodeSetArray(setValues, input);
}
return decodeSetNumber(setValues, value);
}
/**
* @name Set
* @description
* An Set is an array of string values, represented an an encoded type by
* a bitwise representation of the values.
*/
export class CodecSet extends Set<string> implements ISet<string> {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #allowed: SetValues;
readonly #byteLength: number;
constructor (registry: Registry, setValues: SetValues, value?: string[] | Set<string> | Uint8Array | BN | number | string, bitLength = 8) {
super(decodeSet(setValues, value, bitLength));
this.registry = registry;
this.#allowed = setValues;
this.#byteLength = bitLength / 8;
}
public static with (values: SetValues, bitLength?: number): CodecClass<CodecSet> {
return class extends CodecSet {
static {
const keys = Object.keys(values);
const count = keys.length;
const isKeys = new Array<string>(count);
for (let i = 0; i < count; i++) {
isKeys[i] = `is${stringPascalCase(keys[i])}`;
}
objectProperties(this.prototype, isKeys, (_: string, i: number, self: CodecSet) =>
self.strings.includes(keys[i])
);
}
constructor (registry: Registry, value?: string[] | Set<string> | Uint8Array | BN | number | string) {
super(registry, values, value, bitLength);
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.#byteLength;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description true is the Set contains no values
*/
public get isEmpty (): boolean {
return this.size === 0;
}
/**
* @description The actual set values as a string[]
*/
public get strings (): string[] {
return [...super.values()];
}
/**
* @description The encoded value for the set members
*/
public get valueEncoded (): BN {
return encodeSet(this.#allowed, this.strings);
}
/**
* @description adds a value to the Set (extended to allow for validity checking)
*/
public override add = (key: string): this => {
// ^^^ add = () property done to assign this instance's this, otherwise Set.add creates "some" chaos
// we have the isUndefined(this._setValues) in here as well, add is used internally
// in the Set constructor (so it is undefined at this point, and should allow)
if (this.#allowed && isUndefined(this.#allowed[key])) {
throw new Error(`Set: Invalid key '${key}' on add`);
}
super.add(key);
return this;
};
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
if (Array.isArray(other)) {
// we don't actually care about the order, sort the values
return compareArray(this.strings.sort(), other.sort());
} else if (other instanceof Set) {
return this.eq([...other.values()]);
} else if (isNumber(other) || isBn(other as string)) {
return this.valueEncoded.eq(bnToBn(other as string));
}
return false;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
return {
outer: [this.toU8a()]
};
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): string[] {
return this.toJSON();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): string[] {
return this.strings;
}
/**
* @description The encoded value for the set members
*/
public toNumber (): number {
return this.valueEncoded.toNumber();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): string[] {
return this.toJSON();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return stringify({ _set: this.#allowed });
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return `[${this.strings.join(', ')}]`;
}
/**
* @description Encodes the value as a Uint8Array as per the SCALE specifications
*/
public toU8a (_isBare?: boolean): Uint8Array {
return bnToU8a(this.valueEncoded, {
bitLength: this.#byteLength * 8,
isLe: true
});
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,411 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
/* eslint-disable sort-keys */
import type { Bool, Option } from '@pezkuwi/types-codec';
import type { CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { Enum, Struct, Text, U32, Vec } from '@pezkuwi/types-codec';
import { stringify } from '@pezkuwi/util';
import { TEST_A } from './Struct.data.js';
describe('Struct', (): void => {
const registry = new TypeRegistry();
describe('decoding', (): void => {
const testDecode = (type: string, input: any): void =>
it(`can decode from ${type}`, (): void => {
const s = new Struct(registry, {
foo: Text,
bar: U32
}, input);
expect([...s.keys()]).toEqual(['foo', 'bar']);
expect(
[...s.values()].map((v): string =>
v.toString()
)
).toEqual(['bazzing', '69']);
});
testDecode('array', ['bazzing', 69]);
testDecode('hex', '0x1c62617a7a696e6745000000');
testDecode('object', { foo: 'bazzing', bar: 69 });
testDecode('Uint8Array', Uint8Array.from([28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]));
testDecode('Struct.with', new (Struct.with({ foo: 'Text', bar: 'U32' }))(registry, { foo: 'bazzing', bar: 69 }));
it('decodes null/undefined/empty correctly (& equivalent)', (): void => {
const Clazz = Struct.with({
txt: Text,
u32: U32
});
const expected = { txt: '', u32: '0' };
expect(new Clazz(registry, {}).toHuman()).toEqual(expected);
expect(new Clazz(registry, null).toHuman()).toEqual(expected);
expect(new Clazz(registry, undefined).toHuman()).toEqual(expected);
});
it('decodes with Optionals', (): void => {
const Clazz = Struct.with({
a: 'Option<Bool>',
b: 'Option<Bool>'
});
const c = new Clazz(registry, { a: false }) as unknown as { a: Option<Bool>, b: Option<Bool> };
expect(c.a.isSome).toEqual(true);
expect(c.a.unwrap().isTrue).toEqual(false);
expect(c.b.isSome).toEqual(false);
});
it('decodes reusing instantiated inputs', (): void => {
const foo = new Text(registry, 'bar');
expect(
(new Struct(
registry,
{ foo: Text },
{ foo }
)).get('foo')
).toBe(foo);
});
it('decodes a more complicated type', (): void => {
const s = new Struct(registry, {
foo: Vec.with(Struct.with({
bar: Text
}))
}, { foo: [{ bar: 1 }, { bar: 2 }] });
expect(s.toString()).toBe('{"foo":[{"bar":"1"},{"bar":"2"}]}');
});
it('decodes a previously problematic input', (): void => {
let data;
try {
data = new Struct(registry, {
a: 'u32',
b: 'H256',
c: 'H256',
swap: Enum.with({
A: 'u256',
B: 'u256'
}),
d: Vec.with('u8'),
e: 'u8'
}, TEST_A);
} catch (error) {
console.error(error);
throw error;
}
expect(data.get('d')).toHaveLength(50000);
});
it('decodes from a Map input', (): void => {
const s = new Struct(registry, {
txt: Text,
foo: U32,
bar: U32
}, new Map<string, unknown>([['a', 42], ['txt', 'fubar']]));
expect(s.toString()).toEqual('{"txt":"fubar","foo":0,"bar":0}');
});
it('decodes from a snake_case input', (): void => {
const input = new Struct(registry, {
snakeCaseA: U32,
snakeCaseB: Text,
other: U32
}, { snake_case_a: 42, snake_case_b: 'fubar', other: 69 } as any);
expect(input.toString()).toEqual('{"snakeCaseA":42,"snakeCaseB":"fubar","other":69}');
});
it('throws when it cannot decode', (): void => {
expect(
(): Struct<any> => new (
Struct.with({
txt: Text,
u32: U32
})
)(registry, 'ABC')
).toThrow(/Cannot decode value/);
});
it('throws a sensical error on incorrect array values passed to structs', (): void => {
expect(
() => new Struct(registry, {
_: 'Vec<u32>'
}, [123, 456])
).toThrow(/array to object with known keys/);
});
});
describe('encoding', (): void => {
const testEncode = (to: CodecTo, expected: any): void =>
it(`can encode ${to}`, (): void => {
const s = new Struct(registry, {
foo: Text,
bar: U32
}, { foo: 'bazzing', bar: 69 });
expect(s[to]()).toEqual(expected);
});
testEncode('toHex', '0x1c62617a7a696e6745000000');
testEncode('toJSON', { foo: 'bazzing', bar: 69 });
testEncode('toU8a', Uint8Array.from([28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]));
testEncode('toString', '{"foo":"bazzing","bar":69}');
});
it('provides a clean toString()', (): void => {
expect(
new (
Struct.with({
txt: Text,
u32: U32
})
)(registry, { txt: 'foo', u32: 0x123456 }).toString()
).toEqual('{"txt":"foo","u32":1193046}');
});
it('provides a clean toString() (string types)', (): void => {
expect(
new (
Struct.with({
txt: 'Text',
num: 'u32',
cls: U32
})
)(registry, { txt: 'foo', num: 0x123456, cls: 123 }).toString()
).toEqual('{"txt":"foo","num":1193046,"cls":123}');
});
it('exposes the properties on the object', (): void => {
const struct = new (
Struct.with({
txt: Text,
u32: U32
})
)(registry, { txt: 'foo', u32: 0x123456 });
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
expect((struct as any).txt.toString()).toEqual('foo');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
expect((struct as any).u32.toNumber()).toEqual(0x123456);
});
it('correctly encodes length', (): void => {
expect(
new (
Struct.with({
txt: Text,
u32: U32
})
)(registry, { foo: 'bazzing', bar: 69 }).encodedLength
).toEqual(5);
});
it('exposes the types', (): void => {
expect(
new Struct(registry, {
foo: Text,
bar: Text,
baz: U32
}, {
foo: 'foo',
bar: 'bar',
baz: 3
}).Type
).toEqual({
foo: 'Text',
bar: 'Text',
baz: 'u32'
});
});
it('gets the value at a particular index', (): void => {
expect(
new (
Struct.with({
txt: Text,
u32: U32
})
)(registry, { txt: 'foo', u32: 1234 })
.getAtIndex(1)
.toString()
).toEqual('1234');
});
describe('utils', (): void => {
it('compares against other objects', (): void => {
const test = {
foo: 'foo',
bar: 'bar',
baz: 3
};
expect(
new Struct(registry, {
foo: Text,
bar: Text,
baz: U32
}, test).eq(test)
).toBe(true);
});
it('has a sane toPrimitive', (): void => {
const S = Struct.with({
name: 'Text',
description: 'Vec<Text>',
fooA: 'Bytes',
fooB: 'Bytes',
fooC: Struct.with({
a: 'u32',
b: 'u128',
c: 'Compact<u128>',
d: 'bool'
})
});
expect(
new S(registry, {
name: 'Something',
description: ['One line', 'Another line'],
fooA: 'hello world!',
fooB: '0x123456',
fooC: {
a: 1234,
b: BigInt('1234567890111213141516'),
c: 123456,
d: true
}
}).toPrimitive()
).toEqual({
name: 'Something',
description: ['One line', 'Another line'],
fooA: 'hello world!',
fooB: '0x123456',
fooC: {
a: 1234,
b: '1234567890111213141516',
c: 123456,
d: true
}
});
});
it('generates sane toRawType', (): void => {
expect(
new Struct(registry, {
accountId: 'AccountId',
balanceCompact: registry.createClass('Compact<Balance>'),
blockNumber: registry.createClass('BlockNumber'),
compactNumber: registry.createClass('Compact<BlockNumber>'),
optionNumber: registry.createClass('Option<BlockNumber>'),
counter: U32,
vector: Vec.with('AccountId')
}).toRawType()
).toEqual(stringify({
accountId: 'AccountId',
balanceCompact: 'Compact<Balance>', // Override in Uint
blockNumber: 'BlockNumber',
compactNumber: 'Compact<BlockNumber>',
optionNumber: 'Option<BlockNumber>',
counter: 'u32',
vector: 'Vec<AccountId>'
}));
});
it('generates sane toRawType (via with)', (): void => {
const Type = Struct.with({
accountId: 'AccountId',
balance: registry.createClass('Balance')
});
expect(
new Type(registry).toRawType()
).toEqual(stringify({
accountId: 'AccountId',
balance: 'Balance' // Override in Uint
}));
});
});
it('allows toString with large numbers', (): void => {
// replicate https://github.com/pezkuwichain/pezkuwi-api/issues/640
expect(
new Struct(registry, {
blockNumber: registry.createClass('Option<BlockNumber>')
}, { blockNumber: '0x0000000010abcdef' }).toString()
).toEqual('{"blockNumber":279694831}');
});
describe('toU8a', (): void => {
const def: Record<string, any> = {
foo: 'Bytes',
method: 'Bytes',
bar: 'Option<u32>',
baz: 'bool'
};
const val = {
foo: '0x4269',
method: '0x99',
bar: 1,
baz: true
};
it('generates toU8a with undefined', (): void => {
expect(
new Struct(registry, def, val).toU8a()
).toEqual(new Uint8Array([2 << 2, 0x42, 0x69, 1 << 2, 0x99, 1, 1, 0, 0, 0, 1]));
});
it('generates toU8a with true', (): void => {
expect(
new Struct(registry, def, val).toU8a(true)
).toEqual(new Uint8Array([0x42, 0x69, 0x99, 1, 0, 0, 0, 1]));
});
it('generates toU8a with { method: true }', (): void => {
expect(
new Struct(registry, def, val).toU8a({ method: true })
).toEqual(new Uint8Array([2 << 2, 0x42, 0x69, 0x99, 1, 1, 0, 0, 0, 1]));
});
it('has a sane inspect', (): void => {
expect(
new Struct(registry, def, val).inspect()
).toEqual({
inner: [
{
name: 'foo',
outer: [new Uint8Array([2 << 2]), new Uint8Array([0x42, 0x69])]
},
{
name: 'method',
outer: [new Uint8Array([1 << 2]), new Uint8Array([0x99])]
},
{
inner: undefined,
name: 'bar',
outer: [new Uint8Array([1]), new Uint8Array([1, 0, 0, 0])]
},
{
name: 'baz',
outer: [new Uint8Array([1])]
}
]
});
});
});
});
+338
View File
@@ -0,0 +1,338 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyJson, BareOpts, Codec, CodecClass, DefinitionSetter, Inspect, IStruct, IU8a, Registry } from '../types/index.js';
import { isBoolean, isHex, isObject, isU8a, isUndefined, objectProperties, stringCamelCase, stringify, u8aConcatStrict, u8aToHex, u8aToU8a } from '@pezkuwi/util';
import { compareMap, decodeU8aStruct, mapToTypeMap, typesToMap } from '../utils/index.js';
type TypesDef<T = Codec> = Record<string, string | CodecClass<T>>;
type Definition = [CodecClass[], string[]];
function noopSetDefinition (d: Definition): Definition {
return d;
}
/** @internal */
function decodeStructFromObject (registry: Registry, [Types, keys]: Definition, value: any, jsonMap: Map<string, string>): [Iterable<[string, Codec]>, number] {
let jsonObj: Record<string, unknown> | undefined;
const typeofArray = Array.isArray(value);
const typeofMap = value instanceof Map;
const count = keys.length;
if (!typeofArray && !typeofMap && !isObject(value)) {
throw new Error(`Struct: Cannot decode value ${stringify(value)} (typeof ${typeof value}), expected an input object, map or array`);
} else if (typeofArray && value.length !== count) {
throw new Error(`Struct: Unable to map ${stringify(value)} array to object with known keys ${keys.join(', ')}`);
}
const raw = new Array<[string, Codec]>(count);
for (let i = 0; i < count; i++) {
const key = keys[i];
const jsonKey = jsonMap.get(key) || key;
const Type = Types[i];
let assign: unknown;
try {
if (typeofArray) {
assign = value[i] as unknown;
} else if (typeofMap) {
assign = jsonKey && value.get(jsonKey);
} else {
assign = jsonKey && Object.prototype.hasOwnProperty.call(value, jsonKey) ? value[jsonKey] as unknown : undefined;
if (isUndefined(assign)) {
if (isUndefined(jsonObj)) {
const entries = Object.entries(value);
jsonObj = {};
for (let e = 0, ecount = entries.length; e < ecount; e++) {
if (Object.prototype.hasOwnProperty.call(value, entries[e][0])) {
jsonObj[stringCamelCase(entries[e][0])] = entries[e][1];
}
}
}
assign = jsonKey && Object.prototype.hasOwnProperty.call(jsonObj, jsonKey) ? jsonObj[jsonKey] : undefined;
}
}
raw[i] = [
key,
assign instanceof Type
? assign
: new Type(registry, assign)
];
} catch (error) {
let type = Type.name;
try {
type = new Type(registry).toRawType();
} catch {
// ignore
}
throw new Error(`Struct: failed on ${jsonKey}: ${type}:: ${(error as Error).message}`);
}
}
return [raw, 0];
}
/**
* @name Struct
* @description
* A Struct defines an Object with key-value pairs - where the values are Codec values. It removes
* a lot of repetition from the actual coding, define a structure type, pass it the key/Codec
* values in the constructor and it manages the decoding. It is important that the constructor
* values matches 100% to the order in th Rust code, i.e. don't go crazy and make it alphabetical,
* it needs to decoded in the specific defined order.
* @noInheritDoc
*/
export class Struct<
// The actual Class structure, i.e. key -> Class
S extends TypesDef = TypesDef,
// input values, mapped by key can be anything (construction)
V extends { [K in keyof S]: any } = { [K in keyof S]: any },
// type names, mapped by key, name of Class in S
E extends { [K in keyof S]: string } = { [K in keyof S]: string }> extends Map<keyof S, Codec> implements IStruct<keyof S> {
readonly registry: Registry;
public createdAtHash?: IU8a | undefined;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
readonly #jsonMap: Map<keyof S, string>;
readonly #Types: Definition;
constructor (registry: Registry, Types: S, value?: V | Map<unknown, unknown> | unknown[] | HexString | null, jsonMap = new Map<string, string>(), { definition, setDefinition = noopSetDefinition }: DefinitionSetter<Definition> = {}) {
const typeMap = definition || setDefinition(mapToTypeMap(registry, Types));
const [decoded, decodedLength] = isU8a(value) || isHex(value)
? decodeU8aStruct(registry, new Array<[string, Codec]>(typeMap[0].length), u8aToU8a(value), typeMap)
: value instanceof Struct
? [value as Iterable<[string, Codec]>, 0]
: decodeStructFromObject(registry, typeMap, value || {}, jsonMap);
super(decoded);
this.initialU8aLength = decodedLength;
this.registry = registry;
this.#jsonMap = jsonMap;
this.#Types = typeMap;
}
public static with<S extends TypesDef> (Types: S, jsonMap?: Map<string, string>): CodecClass<Struct<S>> {
let definition: Definition | undefined;
// eslint-disable-next-line no-return-assign
const setDefinition = (d: Definition) =>
definition = d;
return class extends Struct<S> {
static {
const keys = Object.keys(Types);
objectProperties(this.prototype, keys, (k: string, _: number, self: Struct) =>
self.get(k)
);
}
constructor (registry: Registry, value?: unknown) {
super(registry, Types, value as HexString, jsonMap, { definition, setDefinition });
}
};
}
/**
* @description The available keys for this struct
*/
public get defKeys (): string[] {
return this.#Types[1];
}
/**
* @description Checks if the value is an empty value '{}'
*/
public get isEmpty (): boolean {
return [...this.keys()].length === 0;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
let total = 0;
for (const v of this.values()) {
total += v.encodedLength;
}
return total;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Returns the Type description of the structure
*/
public get Type (): E {
const result: Record<string, string> = {};
const [Types, keys] = this.#Types;
for (let i = 0, count = keys.length; i < count; i++) {
result[keys[i]] = new Types[i](this.registry).toRawType();
}
return result as E;
}
/**
* @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 specific names entry in the structure
* @param key The name of the entry to retrieve
*/
public override get (key: keyof S): Codec | undefined {
return super.get(key);
}
/**
* @description Returns the values of a member at a specific index (Rather use get(name) for performance)
*/
public getAtIndex (index: number): Codec {
return this.toArray()[index];
}
/**
* @description Returns the a types value by name
*/
public getT <T = Codec> (key: string): T {
return super.get(key) as unknown as T;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (isBare?: BareOpts): Inspect {
const inner: Inspect[] = [];
for (const [k, v] of this.entries()) {
inner.push({
...v.inspect(
!isBare || isBoolean(isBare)
? isBare
: isBare[k]
),
name: stringCamelCase(k as string)
});
}
return {
inner
};
}
/**
* @description Converts the Object to an standard JavaScript Array
*/
public toArray (): Codec[] {
return [...this.values()];
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
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, disableAscii?: boolean): Record<string, AnyJson> {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
json[k as string] = v.toHuman(isExtended, disableAscii);
}
return json;
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): Record<string, AnyJson> {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
// Here we pull out the entry against the JSON mapping (if supplied)
// since this representation goes over RPC and needs to be correct
json[(this.#jsonMap.get(k) || k) as string] = v.toJSON();
}
return json;
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (disableAscii?: boolean): Record<string, AnyJson> {
const json: Record<string, AnyJson> = {};
for (const [k, v] of this.entries()) {
json[k as string] = v.toPrimitive(disableAscii);
}
return json;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return stringify(typesToMap(this.registry, this.#Types));
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return 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?: BareOpts): Uint8Array {
const encoded: Uint8Array[] = [];
for (const [k, v] of this.entries()) {
encoded.push(
v.toU8a(
!isBare || isBoolean(isBare)
? isBare
: isBare[k]
)
);
}
return u8aConcatStrict(encoded);
}
}
@@ -0,0 +1,85 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { CodecTo } from '@pezkuwi/types-codec/types';
import { TypeRegistry } from '@pezkuwi/types';
import { Bytes, Raw, Text } from '@pezkuwi/types-codec';
import { stringToU8a } from '@pezkuwi/util';
import { perf } from '../test/performance.js';
describe('Text', (): void => {
const registry = new TypeRegistry();
describe('decode', (): void => {
const testDecode = (type: string, input: null | string | Uint8Array | { toString: () => string }, expected: string, toFn: 'toString' | 'toHex' | 'toHuman' = 'toString'): void =>
it(`can decode from ${type}`, (): void => {
expect(new Text(registry, input)[toFn]()).toBe(expected);
});
testDecode('string', 'foo', 'foo');
testDecode('Text', new Text(registry, 'foo'), 'foo');
testDecode('Uint8Array', Uint8Array.from([12, 102, 111, 111]), 'foo');
testDecode('Raw', new Raw(registry, Uint8Array.from([102, 111, 111])), 'foo'); // no length
testDecode('Raw', new Raw(registry, Uint8Array.from([102, 111, 111])), 'foo', 'toHuman'); // no length
testDecode('Bytes', new Bytes(registry, Uint8Array.from([12, 102, 111, 111])), 'foo'); // length-aware encoding
testDecode('Bytes', new Bytes(registry, Uint8Array.from([12, 102, 111, 111])), 'foo', 'toHuman'); // length-aware encoding
testDecode('object with `toString()`', { toString (): string {
return 'foo';
} }, 'foo');
testDecode('hex input value', new Text(registry, '0x12345678'), '0x12345678', 'toHex');
testDecode('null', null, '');
});
describe('encode', (): void => {
const testEncode = (to: CodecTo, expected: string | Uint8Array): void =>
it(`can encode ${to}`, (): void => {
expect(new Text(registry, 'foo')[to]()).toEqual(expected);
});
testEncode('toHex', '0x666f6f');
testEncode('toString', 'foo');
testEncode('toU8a', Uint8Array.from([12, 102, 111, 111]));
});
describe('utils', (): void => {
it('compares actual string values', (): void => {
expect(new Text(registry, '123').eq('123')).toBe(true);
});
it('compares actual String values', (): void => {
expect(new Text(registry, 'XYX').eq(String('XYX'))).toBe(true);
});
it('compares actual non-string values (fails)', (): void => {
expect(new Text(registry, '123').eq(123)).toBe(false);
});
it('calulates the length & encoded length correctly for ASCII', (): void => {
const test = new Text(registry, 'abcde');
expect(test.encodedLength).toEqual(6);
expect(test).toHaveLength(5);
});
it('calulates the length & encoded length correctly for non-ASCII', (): void => {
const test = new Text(registry, '中文');
expect(test.encodedLength).toEqual(7);
expect(test).toHaveLength(2);
});
it('has a sane inspect', (): void => {
expect(
new Text(registry, 'abcde').inspect()
).toEqual({
outer: [new Uint8Array([5 << 2]), stringToU8a('abcde')]
});
});
});
perf('Text', 100_000, [[new Uint8Array([6 << 2, 102, 111, 111, 102, 111, 111])]], (v: Uint8Array) => new Text(registry, v));
});
+184
View File
@@ -0,0 +1,184 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import type { AnyString, AnyU8a, Inspect, IText, IU8a, Registry } from '../types/index.js';
import { compactAddLength, compactFromU8aLim, compactToU8a, hexToU8a, isHex, isString, isU8a, stringToU8a, u8aToHex, u8aToString } from '@pezkuwi/util';
import { Raw } from './Raw.js';
const MAX_LENGTH = 128 * 1024;
/** @internal */
function decodeText (value?: null | AnyString | AnyU8a | { toString: () => string }): [string, number] {
if (isU8a(value)) {
if (!value.length) {
return ['', 0];
}
// for Raw, the internal buffer does not have an internal length
// (the same applies in e.g. Bytes, where length is added at encoding-time)
if (value instanceof Raw) {
return [u8aToString(value), 0];
}
const [offset, length] = compactFromU8aLim(value);
const total = offset + length;
if (length > MAX_LENGTH) {
throw new Error(`Text: length ${length.toString()} exceeds ${MAX_LENGTH}`);
} else if (total > value.length) {
throw new Error(`Text: required length less than remainder, expected at least ${total}, found ${value.length}`);
}
return [u8aToString(value.subarray(offset, total)), total];
} else if (isHex(value)) {
return [u8aToString(hexToU8a(value)), 0];
}
return [value ? value.toString() : '', 0];
}
/**
* @name Text
* @description
* This is a string wrapper, along with the length. It is used both for strings as well
* as items such as documentation. It simply extends the standard JS `String` built-in
* object, inheriting all methods exposed from `String`.
* @noInheritDoc
*/
export class Text extends String implements IText {
readonly registry: Registry;
public createdAtHash?: IU8a;
public initialU8aLength?: number;
public isStorageFallback?: boolean;
#override: string | null = null;
constructor (registry: Registry, value?: null | AnyString | AnyU8a | { toString: () => string }) {
const [str, decodedLength] = decodeText(value);
super(str);
this.registry = registry;
this.initialU8aLength = decodedLength;
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return this.toU8a().length;
}
/**
* @description returns a hash of the contents
*/
public get hash (): IU8a {
return this.registry.hash(this.toU8a());
}
/**
* @description Checks if the value is an empty value
*/
public get isEmpty (): boolean {
return this.length === 0;
}
/**
* @description The length of the value
*/
public override get length (): number {
// only included here since we ignore inherited docs
return super.length;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: unknown): boolean {
return isString(other)
? this.toString() === other.toString()
: false;
}
/**
* @description Returns a breakdown of the hex encoding for this Codec
*/
public inspect (): Inspect {
const value = stringToU8a(super.toString());
return {
outer: value.length
? [compactToU8a(value.length), value]
: [compactToU8a(value.length)]
};
}
/**
* @description Set an override value for this
*/
public setOverride (override: string): void {
this.#override = override;
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): HexString {
// like with Vec<u8>, when we are encoding to hex, we don't actually add
// the length prefix (it is already implied by the actual string length)
return u8aToHex(this.toU8a(true));
}
/**
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
*/
public toHuman (): string {
return this.toJSON();
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): string {
return this.toString();
}
/**
* @description Converts the value in a best-fit primitive form
*/
public toPrimitive (): string {
return this.toJSON();
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return 'Text';
}
/**
* @description Returns the string representation of the value
*/
public override toString (): string {
return this.#override || super.toString();
}
/**
* @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 {
// NOTE Here we use the super toString (we are not taking overrides into account,
// rather encoding the original value the string was constructed with)
const encoded = stringToU8a(super.toString());
return isBare
? encoded
: compactAddLength(encoded);
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { bool as Bool, bool } from './Bool.js';
export { CodecDate, CodecDate as Date } from './Date.js';
export { Float } from './Float.js';
export { Json } from './Json.js';
export { Raw } from './Raw.js';
export { CodecSet, CodecSet as Set } from './Set.js';
export { Struct } from './Struct.js';
export { Text } from './Text.js';
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2026 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
// (packageInfo imports will be kept as-is, user-editable)
import { detectPackage } from '@pezkuwi/util';
import { packageInfo } from './packageInfo.js';
detectPackage(packageInfo, null, []);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2026 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/types-codec', path: 'auto', type: 'auto', version: '16.5.4' };
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Float } from '../native/Float.js';
/**
* @name f32
* @description
* A 32-bit float
*/
export class f32 extends Float.with(32) {
// NOTE without this, we cannot properly determine extensions
readonly __FloatType = 'f32';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Float } from '../native/Float.js';
/**
* @name f64
* @description
* A 64-bit float
*/
export class f64 extends Float.with(64) {
// NOTE without this, we cannot properly determine extensions
readonly __FloatType = 'f64';
}
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i128
* @description
* A 128-bit signed integer
*/
export class i128 extends Int.with(128) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i128';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i16
* @description
* A 16-bit signed integer
*/
export class i16 extends Int.with(16) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i16';
}
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i256
* @description
* A 256-bit signed integer
*/
export class i256 extends Int.with(256) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i256';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i32
* @description
* A 32-bit signed integer
*/
export class i32 extends Int.with(32) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i32';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i64
* @description
* A 64-bit signed integer
*/
export class i64 extends Int.with(64) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i64';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Int } from '../base/Int.js';
/**
* @name i8
* @description
* An 8-bit signed integer
*/
export class i8 extends Int.with(8) {
// NOTE without this, we cannot properly determine extensions
readonly __IntType = 'i8';
}
@@ -0,0 +1,21 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registry } from '../types/index.js';
import { i32 } from './I32.js';
/**
* @name ISize
* @description
* A System default signed number, typically used in RPC to report non-consensus
* data. It is a wrapper for [[I32]] as a WASM default (as generated by Rust bindings).
* It is not to be used, since it creates consensus mismatches.
*/
export class isize extends i32 {
constructor (registry: Registry, value?: unknown) {
super(registry, value);
throw new Error('The `isize` type should not be used. Since it is platform-specific, it creates incompatibilities between native (generally i64) and WASM (always i32) code. Use one of the `i32` or `i64` types explicitly.');
}
}
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u128
* @description
* A 128-bit unsigned integer
*/
export class u128 extends UInt.with(128) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u128';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u16
* @description
* A 16-bit unsigned integer
*/
export class u16 extends UInt.with(16) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u16';
}
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u256
* @description
* A 256-bit unsigned integer
*/
export class u256 extends UInt.with(256) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u256';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u32
* @description
* A 32-bit unsigned integer
*/
export class u32 extends UInt.with(32) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u32';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u64
* @description
* A 64-bit unsigned integer
*/
export class u64 extends UInt.with(64) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u64';
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { UInt } from '../base/UInt.js';
/**
* @name u8
* @description
* An 8-bit unsigned integer
*/
export class u8 extends UInt.with(8) {
// NOTE without this, we cannot properly determine extensions
readonly __UIntType = 'u8';
}
@@ -0,0 +1,21 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registry } from '../types/index.js';
import { u32 } from './U32.js';
/**
* @name USize
* @description
* A System default unsigned number, typically used in RPC to report non-consensus
* data. It is a wrapper for [[U32]] as a WASM default (as generated by Rust bindings).
* It is not to be used, since it created consensus mismatches.
*/
export class usize extends u32 {
constructor (registry: Registry, value?: unknown) {
super(registry, value);
throw new Error('The `usize` type should not be used. Since it is platform-specific, it creates incompatibilities between native (generally u64) and WASM (always u32) code. Use one of the `u32` or `u64` types explicitly.');
}
}
@@ -0,0 +1,19 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { f32 as F32, f32 } from './F32.js';
export { f64 as F64, f64 } from './F64.js';
export { i8 as I8, i8 } from './I8.js';
export { i16 as I16, i16 } from './I16.js';
export { i32 as I32, i32 } from './I32.js';
export { i64 as I64, i64 } from './I64.js';
export { i128 as I128, i128 } from './I128.js';
export { i256 as I256, i256 } from './I256.js';
export { isize as ISize, isize } from './ISize.js';
export { u8 as U8, u8 } from './U8.js';
export { u16 as U16, u16 } from './U16.js';
export { u32 as U32, u32 } from './U32.js';
export { u64 as U64, u64 } from './U64.js';
export { u128 as U128, u128 } from './U128.js';
export { u256 as U256, u256 } from './U256.js';
export { usize as USize, usize } from './USize.js';
@@ -0,0 +1,61 @@
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global it */
// Shamelessly copied from @pezkuwi/util/test
import { formatDecimal, formatNumber } from '@pezkuwi/util';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExecFn = (...params: any[]) => unknown;
const NUM_PAD = 16;
const PRE_PAD = 32;
function loop (count: number, inputs: unknown[][], exec: ExecFn): [number, unknown[]] {
const start = performance.now();
const inputsCount = inputs.length;
const results = new Array<unknown>(inputsCount);
for (let i = 0; i < count; i++) {
const result = exec(...inputs[i % inputsCount]);
if (i < inputsCount) {
results[i] = result;
}
}
return [performance.now() - start, results];
}
export function formatFixed (value: number): string {
const [a, b] = value.toFixed(2).split('.');
return [formatDecimal(a), b].join('.');
}
export function formatOps (count: number, time: number): string {
const micro = (time * 1000) / count;
const ops = 1_000_000 / micro;
return `
${formatFixed(ops).padStart(NUM_PAD + PRE_PAD + 1)} ops/s
${formatFixed(micro).padStart(NUM_PAD + PRE_PAD + 1)} μs/op`;
}
export function perf (name: string, count: number, inputs: unknown[][], exec: ExecFn): void {
const test = process.env['GITHUB_REPOSITORY']
? it.skip
: it;
test(`performance: ${name}`, (): void => {
const [time] = loop(count, inputs, exec);
console.error(`
performance run for ${name} completed with ${formatNumber(count)} iterations.
${`${name}:`.padStart(PRE_PAD)} ${time.toFixed(2).padStart(NUM_PAD)} ms${formatOps(count, time)}
`);
});
}

Some files were not shown because too many files have changed in this diff Show More