From 528b4f9317ae6ab33b7d62815709d953adb85d9b Mon Sep 17 00:00:00 2001 From: Jaco Date: Thu, 26 Jan 2023 12:15:51 +0200 Subject: [PATCH 1/7] Add BigInt composite --- packages/types-codec/src/abstract/Int.ts | 12 +- .../types-codec/src/abstract/IntBigInt.ts | 276 ++++++++++++++++++ packages/types-codec/src/types/helpers.ts | 4 + 3 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 packages/types-codec/src/abstract/IntBigInt.ts diff --git a/packages/types-codec/src/abstract/Int.ts b/packages/types-codec/src/abstract/Int.ts index 1947cfd52e07..e2a0b5a00be6 100644 --- a/packages/types-codec/src/abstract/Int.ts +++ b/packages/types-codec/src/abstract/Int.ts @@ -29,7 +29,7 @@ function toPercentage (value: BN, divisor: BN): string { } /** @internal */ -function decodeAbstractInt (value: Exclude | Record | ToBn, isNegative: boolean): string | number { +function decodeBN (value: Exclude | Record | ToBn, 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'); @@ -62,7 +62,7 @@ function decodeAbstractInt (value: Exclude | Record type === rawType) || []; diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts new file mode 100644 index 000000000000..b3e0b5aba82a --- /dev/null +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -0,0 +1,276 @@ +// Copyright 2017-2023 @polkadot/types-codec authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { HexString } from '@polkadot/util/types'; +import type { AnyNumber, Inspect, INumber, Registry, UIntBitLength, ToBigInt } from '../types'; + +import { _0n, _1n, _2n, _100n, _1Bn, _1Mn, _1Qn, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, u8aToBigInt } from '@polkadot/util'; +import { BigInt } from '@polkadot/x-bigint'; + +import { AbstractObject } from './Object'; + +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 = BigInt(1_00_00); + +const FORMATTERS: Record = { + Perbill: _1Bn, + Percent: _100n, + Permill: _1Mn, + Perquintill: _1Qn +}; + +function isToBigInt (value: unknown): value is ToBigInt { + return isFunction((value as ToBigInt).toBigInt); +} + +function toPercentage (value: bigint, divisor: bigint): string { + return `${(Number((value * MUL_P) / divisor) / 100).toFixed(2)}%`; +} + +/** @internal */ +function decodeBigInt (value: Exclude | Record, isNegative: boolean): bigint { + if (isBigInt(value)) { + return value; + } else 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 BigInt(value); + } else if (isString(value)) { + if (isHex(value, -1, true)) { + return hexToBigInt(value, { isLe: false, isNegative }); + } + + if (value.includes('.') || value.includes(',') || value.includes('e')) { + throw new Error('String should not contain decimal points or scientific notation'); + } + + return BigInt(value); + } else if (isBn(value)) { + return BigInt(value.toString()); + } else if (isObject(value)) { + if (isToBigInt(value)) { + return value.toBigInt(); + } + + // 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 decodeBigInt(value[keys[0]], isNegative); + } + + throw new Error(`Unable to create BigInt from unknown type ${typeof value}`); +} + +/** + * @name AbstractInt + * @ignore + * @noInheritDoc + */ +export abstract class AbstractBigInt extends AbstractObject implements INumber { + readonly isUnsigned: boolean; + + readonly #bitLength: UIntBitLength; + readonly #bitLengthInitial: number; + readonly #encodedLength: number; + + constructor (registry: Registry, value: AnyNumber = 0, bitLength: UIntBitLength = DEFAULT_UINT_BITS, isSigned = false) { + super( + registry, + // shortcut isU8a as used in SCALE decoding + isU8a(value) + ? bitLength <= 48 + ? u8aToBigInt(value.subarray(0, bitLength / 8), { isNegative: isSigned }) + : u8aToBigInt(value.subarray(0, bitLength / 8), { isLe: true, isNegative: isSigned }) + : decodeBigInt(value, isSigned), + bitLength / 8 + ); + + this.#bitLength = bitLength; + this.#bitLengthInitial = this.$.toString(2).length; + this.#encodedLength = this.#bitLength / 8; + this.isUnsigned = !isSigned; + + const isNegative = this.$ < _0n; + const maxBits = bitLength - (isSigned && !isNegative ? 1 : 0); + + if (isNegative && !isSigned) { + throw new Error(`${this.toRawType()}: Negative number passed to unsigned type`); + } else if (this.#bitLengthInitial > maxBits) { + throw new Error(`${this.toRawType()}: Input too large. Found input with ${this.#bitLengthInitial} bits, expected ${maxBits}`); + } + } + + public override get encodedLength (): number { + return this.#encodedLength; + } + + /** + * @description Checks if the value is a zero value (align elsewhere) + */ + public get isEmpty (): boolean { + return this.$ === _0n; + } + + /** + * @description Returns the number of bits in the value + */ + public bitLength (): number { + return this.#bitLength; + } + + /** + * @description Compares the value of the input to see if there is a match + */ + public override eq (other?: unknown): boolean { + return this.$ === ( + isHex(other) + ? hexToBigInt(other.toString(), { isLe: false, isNegative: !this.isUnsigned }) + : nToBigInt(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 this.$; + } + + /** + * @description Returns the BN representation of the number. (Compatibility) + */ + public toBn (): BN { + return bnToBn(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 + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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[rawType]; + + return divisor + ? toPercentage(this.$, divisor) + : formatNumber(this.$); + } + + /** + * @description Converts the Object to JSON, typically used for RPC transfers + */ + public override toJSON (onlyHex = false): string | number { + // Options here are + // - this.#bitLengthInitial - the actual used bits + // - this.#bitLength - the type bits (this should be used, however contracts RPC is problematic) + return onlyHex || (this.#bitLengthInitial > MAX_NUMBER_BITS) + ? this.toHex() + : this.toNumber(); + } + + /** + * @description Returns the number representation of this value (only for < MAX_SAFE_INTEGER) + */ + public toNumber (): number { + return Number(this.$); + } + + /** + * @description Returns the value in a primitive form, either number when <= 52 bits, or string otherwise + */ + public toPrimitive (): number | string { + return this.#bitLengthInitial > 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 this.$.toString(base); + } + + /** + * @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) + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public toU8a (isBare?: boolean): Uint8Array { + return bnToU8a(this, { + bitLength: this.bitLength(), + isLe: true, + isNegative: !this.isUnsigned + }); + } +} diff --git a/packages/types-codec/src/types/helpers.ts b/packages/types-codec/src/types/helpers.ts index b809207f8083..2f2ad77fd43b 100644 --- a/packages/types-codec/src/types/helpers.ts +++ b/packages/types-codec/src/types/helpers.ts @@ -42,4 +42,8 @@ export interface ToBn { toBn: () => BN; } +export interface ToBigInt { + toBigInt: () => bigint; +} + export type LookupString = `Lookup${number}`; From f9c887fc43babe449495c5e1928e907f8320a716 Mon Sep 17 00:00:00 2001 From: Jaco Date: Thu, 26 Jan 2023 12:18:14 +0200 Subject: [PATCH 2/7] Adjust --- packages/types-codec/src/abstract/IntBigInt.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index b3e0b5aba82a..c500d6ef1e3d 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -91,9 +91,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I registry, // shortcut isU8a as used in SCALE decoding isU8a(value) - ? bitLength <= 48 - ? u8aToBigInt(value.subarray(0, bitLength / 8), { isNegative: isSigned }) - : u8aToBigInt(value.subarray(0, bitLength / 8), { isLe: true, isNegative: isSigned }) + ? u8aToBigInt(value.subarray(0, bitLength / 8), { isLe: true, isNegative: isSigned }) : decodeBigInt(value, isSigned), bitLength / 8 ); From 15372b9b430830f6c352fc879f4316ad8a26d072 Mon Sep 17 00:00:00 2001 From: Jaco Date: Sat, 28 Jan 2023 11:46:09 +0200 Subject: [PATCH 3/7] Adjust imports --- packages/types-codec/src/abstract/IntBigInt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index c500d6ef1e3d..54ab1fcb0aa6 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -3,9 +3,9 @@ import type { BN } from '@polkadot/util'; import type { HexString } from '@polkadot/util/types'; -import type { AnyNumber, Inspect, INumber, Registry, UIntBitLength, ToBigInt } from '../types'; +import type { AnyNumber, Inspect, INumber, Registry, ToBigInt, UIntBitLength } from '../types'; -import { _0n, _1n, _2n, _100n, _1Bn, _1Mn, _1Qn, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, u8aToBigInt } from '@polkadot/util'; +import { _0n, _1Bn, _1Mn, _1Qn, _100n, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, u8aToBigInt } from '@polkadot/util'; import { BigInt } from '@polkadot/x-bigint'; import { AbstractObject } from './Object'; From 4ee934f73d4548833b5b15dc0f65d9626bd8c62d Mon Sep 17 00:00:00 2001 From: Jaco Date: Sat, 28 Jan 2023 11:49:28 +0200 Subject: [PATCH 4/7] Hewx/u8a conversions --- packages/types-codec/src/abstract/IntBigInt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index 54ab1fcb0aa6..bb8ec1a0352f 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -5,7 +5,7 @@ import type { BN } from '@polkadot/util'; import type { HexString } from '@polkadot/util/types'; import type { AnyNumber, Inspect, INumber, Registry, ToBigInt, UIntBitLength } from '../types'; -import { _0n, _1Bn, _1Mn, _1Qn, _100n, bnToBn, bnToHex, bnToU8a, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, u8aToBigInt } from '@polkadot/util'; +import { _0n, _1Bn, _1Mn, _1Qn, _100n, bnToBn, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, nToHex, nToU8a, u8aToBigInt } from '@polkadot/util'; import { BigInt } from '@polkadot/x-bigint'; import { AbstractObject } from './Object'; @@ -177,7 +177,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I */ public toHex (isLe = false): HexString { // For display/JSON, this is BE, for compare, use isLe - return bnToHex(this, { + return nToHex(this.$, { bitLength: this.bitLength(), isLe, isNegative: !this.isUnsigned @@ -265,7 +265,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public toU8a (isBare?: boolean): Uint8Array { - return bnToU8a(this, { + return nToU8a(this.$, { bitLength: this.bitLength(), isLe: true, isNegative: !this.isUnsigned From ae702a2ada0f5606971213acb1d189bfbd8e2f53 Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 29 Aug 2023 16:45:47 +0300 Subject: [PATCH 5/7] Align with latest AbstractInt changes --- packages/types-codec/src/abstract/IntBigInt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index bb8ec1a0352f..df99802eda65 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -3,12 +3,12 @@ import type { BN } from '@polkadot/util'; import type { HexString } from '@polkadot/util/types'; -import type { AnyNumber, Inspect, INumber, Registry, ToBigInt, UIntBitLength } from '../types'; +import type { AnyNumber, Inspect, INumber, Registry, ToBigInt, UIntBitLength } from '../types/index.js'; import { _0n, _1Bn, _1Mn, _1Qn, _100n, bnToBn, formatBalance, formatNumber, hexToBigInt, isBigInt, isBn, isFunction, isHex, isNumber, isObject, isString, isU8a, nToBigInt, nToHex, nToU8a, u8aToBigInt } from '@polkadot/util'; import { BigInt } from '@polkadot/x-bigint'; -import { AbstractObject } from './Object'; +import { AbstractObject } from './Object.js'; export const DEFAULT_UINT_BITS = 64; @@ -217,7 +217,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I // Options here are // - this.#bitLengthInitial - the actual used bits // - this.#bitLength - the type bits (this should be used, however contracts RPC is problematic) - return onlyHex || (this.#bitLengthInitial > MAX_NUMBER_BITS) + return onlyHex || (this.#bitLength > 128) || (this.#bitLengthInitial > MAX_NUMBER_BITS) ? this.toHex() : this.toNumber(); } From e455f697f9eb8920dc76acb6fecd74dee1cd1829 Mon Sep 17 00:00:00 2001 From: Jaco Date: Wed, 30 Aug 2023 08:07:20 +0300 Subject: [PATCH 6/7] _ to unused vars --- packages/types-codec/src/abstract/IntBigInt.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index df99802eda65..ee4a34b15bab 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -187,8 +187,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I /** * @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public toHuman (isExpanded?: boolean): string { + public toHuman (_isExpanded?: boolean): string { const rawType = this.toRawType(); if (rawType === 'Balance') { @@ -263,8 +262,7 @@ export abstract class AbstractBigInt extends AbstractObject implements I * @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) */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public toU8a (isBare?: boolean): Uint8Array { + public toU8a (_isBare?: boolean): Uint8Array { return nToU8a(this.$, { bitLength: this.bitLength(), isLe: true, From 351cfe1b39aaa62c8497b0ff75673605bf4ab6a5 Mon Sep 17 00:00:00 2001 From: Jaco Date: Wed, 30 Aug 2023 08:07:45 +0300 Subject: [PATCH 7/7] Adjust --- packages/types-codec/src/abstract/IntBigInt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types-codec/src/abstract/IntBigInt.ts b/packages/types-codec/src/abstract/IntBigInt.ts index ee4a34b15bab..64ea8941b4ee 100644 --- a/packages/types-codec/src/abstract/IntBigInt.ts +++ b/packages/types-codec/src/abstract/IntBigInt.ts @@ -75,7 +75,7 @@ function decodeBigInt (value: Exclude | Record