mirror of
https://github.com/pezkuwichain/pezkuwi-api.git
synced 2026-04-22 09:07:56 +00:00
Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
@@ -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}>`;
|
||||
}
|
||||
}
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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()}>`;
|
||||
}
|
||||
}
|
||||
@@ -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')] }
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}]`;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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])]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -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])]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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, []);
|
||||
@@ -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' };
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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)}
|
||||
`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
import type { AnyJson, ToString } from './helpers.js';
|
||||
import type { IU8a } from './interfaces.js';
|
||||
import type { Registry } from './registry.js';
|
||||
|
||||
export type BareOpts = boolean | Record<string, boolean>;
|
||||
|
||||
export interface Inspect {
|
||||
inner?: Inspect[] | undefined;
|
||||
name?: string;
|
||||
outer?: Uint8Array[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Codec
|
||||
* @description
|
||||
* The base Codec interface. All types implement the interface provided here.
|
||||
* Additionally implementors can add their own specific interfaces and helpers
|
||||
* with getters and functions. The Codec Base is however required for operating
|
||||
* as an encoding/decoding layer
|
||||
*/
|
||||
export interface Codec {
|
||||
/**
|
||||
* @description
|
||||
* The block at which this value was retrieved/created (set to non-empty when
|
||||
* retrieved from storage)
|
||||
*/
|
||||
createdAtHash?: IU8a | undefined;
|
||||
|
||||
/**
|
||||
* @description
|
||||
* The length of the initial encoded value (Only available when the value was
|
||||
* constructed from a Uint8Array input)
|
||||
*/
|
||||
initialU8aLength?: number | undefined;
|
||||
|
||||
/**
|
||||
* @description
|
||||
* (internal usage) Indicates that the value was created via a fallback. This
|
||||
* is used when with data specified in the metadata when the storage entry is
|
||||
* empty.
|
||||
*
|
||||
* With metadata fallback values (available as defaults on most storage entries)
|
||||
* any empty storage item should erturn the default. (This is the same as the
|
||||
* implementation on the Bizinikiwi runtime)
|
||||
*/
|
||||
isStorageFallback?: boolean;
|
||||
|
||||
/**
|
||||
* @description The length of the value when encoded as a Uint8Array
|
||||
*/
|
||||
readonly encodedLength: number;
|
||||
|
||||
/**
|
||||
* @description Returns a hash of the value
|
||||
*/
|
||||
readonly hash: IU8a;
|
||||
|
||||
/**
|
||||
* @description Checks if the value is an empty value
|
||||
*/
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
/**
|
||||
* @description The registry associated with this object
|
||||
*/
|
||||
readonly registry: Registry;
|
||||
|
||||
/**
|
||||
* @description Compares the value of the input to see if there is a match
|
||||
*/
|
||||
eq (other?: unknown): boolean;
|
||||
|
||||
/**
|
||||
* @description Returns a breakdown of the hex encoding for this Codec
|
||||
*/
|
||||
inspect (isBare?: BareOpts): Inspect;
|
||||
|
||||
/**
|
||||
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
|
||||
*/
|
||||
toHex (isLe?: boolean): HexString;
|
||||
|
||||
/**
|
||||
* @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information
|
||||
* @param isExtended When set, for some (e.g. call) it can add more info, e.g. metadata documentation. (Generally not needed in all cases, but can be useful in Events, Calls, ...)
|
||||
* @param disableAscii When set, for some (e.g. `Raw` types) it will disable converting the value to ascii.
|
||||
*/
|
||||
toHuman (isExtended?: boolean, disableAscii?: boolean): AnyJson;
|
||||
|
||||
/**
|
||||
* @description Converts the Object to JSON, typically used for RPC transfers
|
||||
*/
|
||||
toJSON (): AnyJson;
|
||||
|
||||
/**
|
||||
* @description Converts the value in a best-fit primitive form
|
||||
* @param disableAscii
|
||||
*/
|
||||
toPrimitive (disableAscii?: boolean): AnyJson;
|
||||
|
||||
/**
|
||||
* @description Returns the base runtime type name for this instance
|
||||
*/
|
||||
toRawType (): string;
|
||||
|
||||
/**
|
||||
* @description Returns the string representation of the value
|
||||
*/
|
||||
toString (): string;
|
||||
|
||||
/**
|
||||
* @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 use, only available on
|
||||
* some Codec types, specifically those that add encodings such as length of indexes)
|
||||
*/
|
||||
toU8a (isBare?: BareOpts): Uint8Array;
|
||||
}
|
||||
|
||||
export interface CodecClass<T = Codec, A extends unknown[] = any[]> {
|
||||
/**
|
||||
* @description An internal fallback type (previous generation) if encoding fails
|
||||
*/
|
||||
readonly __fallbackType?: string;
|
||||
|
||||
new(registry: Registry, ...args: A): T;
|
||||
}
|
||||
|
||||
export interface CodecObject<T extends ToString> extends Codec {
|
||||
readonly $: T;
|
||||
|
||||
valueOf (): T;
|
||||
}
|
||||
|
||||
export type CodecTo = 'toHex' | 'toJSON' | 'toPrimitive' | 'toString' | 'toU8a';
|
||||
|
||||
export type ArgsDef = Record<string, CodecClass | string>;
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 { Codec } from './codec.js';
|
||||
|
||||
export type AnyJson = string | number | boolean | null | undefined | AnyJson[] | { [index: string]: AnyJson };
|
||||
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
export type AnyNumber = BN | bigint | Uint8Array | number | string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type AnyFloat = Number | number | Uint8Array | string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type AnyString = String | string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type AnyBool = Boolean | boolean;
|
||||
|
||||
export type AnyTuple = Codec[];
|
||||
|
||||
export type AnyU8a = Uint8Array | number[] | string;
|
||||
|
||||
export type UIntBitLength = 8 | 16 | 32 | 64 | 128 | 256;
|
||||
|
||||
// The 520 here is a weird one - it is explicitly for a [u8; 65] as found as a EcdsaSignature,
|
||||
// and 264 here is explicitly for a [u8; 33] as found as EcdsaPublic key.
|
||||
// Likewise 160 is for [u8; 20], which is also a H160, i.e. an Ethereum address. Both these are
|
||||
// as a result of the Pezkuwi claims module. (Technically we don't need the 520 in here)
|
||||
export type U8aBitLength = 8 | 16 | 32 | 64 | 128 | 160 | 256 | 264 | 512 | 520 | 1024 | 2048;
|
||||
|
||||
export type AnyTupleValue = Exclude<AnyU8a, string> | HexString | (Codec | AnyU8a | AnyNumber | AnyString | undefined | null)[];
|
||||
|
||||
export interface ToString {
|
||||
toString: () => string;
|
||||
}
|
||||
|
||||
export interface ToBn {
|
||||
toBn: () => BN;
|
||||
}
|
||||
|
||||
export interface DefinitionSetter <T> {
|
||||
definition?: T | undefined;
|
||||
setDefinition?: (d: T) => T;
|
||||
}
|
||||
|
||||
export type LookupString = `Lookup${number}`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user