From 91a04ad1593240b059fd86a489d0049ee3e0a595 Mon Sep 17 00:00:00 2001 From: Jaco Date: Mon, 18 Jul 2022 13:10:46 +0200 Subject: [PATCH] Add Codec toPrimitive (#5080) --- CHANGELOG.md | 1 + packages/types-codec/src/abstract/Array.ts | 13 ++++++++++++ packages/types-codec/src/abstract/Base.ts | 7 +++++++ packages/types-codec/src/abstract/Int.ts | 9 ++++++++ packages/types-codec/src/base/Compact.ts | 7 +++++++ .../types-codec/src/base/DoNotConstruct.ts | 4 ++++ packages/types-codec/src/base/Enum.ts | 9 ++++++++ packages/types-codec/src/base/Null.ts | 7 +++++++ packages/types-codec/src/base/Option.ts | 9 ++++++++ packages/types-codec/src/extended/BTreeSet.ts | 13 ++++++++++++ packages/types-codec/src/extended/Map.ts | 17 +++++++++++++++ .../src/extended/WrapperKeepOpaque.ts | 9 ++++++++ packages/types-codec/src/native/Bool.ts | 7 +++++++ packages/types-codec/src/native/Date.ts | 7 +++++++ packages/types-codec/src/native/Float.ts | 7 +++++++ packages/types-codec/src/native/Json.ts | 13 ++++++++++++ packages/types-codec/src/native/Raw.spec.ts | 6 ++++++ packages/types-codec/src/native/Raw.ts | 21 ++++++++++++------- packages/types-codec/src/native/Set.ts | 7 +++++++ packages/types-codec/src/native/Struct.ts | 17 +++++++++++++++ packages/types-codec/src/native/Text.ts | 7 +++++++ packages/types-codec/src/types/codec.ts | 7 ++++++- packages/types-codec/src/types/interfaces.ts | 1 + packages/types/src/ethereum/AccountId.ts | 7 +++++++ .../types/src/extrinsic/Extrinsic.spec.ts | 1 + packages/types/src/generic/AccountId.ts | 7 +++++++ packages/types/src/generic/AccountIndex.ts | 7 +++++++ packages/types/src/generic/Call.ts | 7 +++++++ packages/types/src/generic/Vote.spec.ts | 20 ++++++++++++------ packages/types/src/generic/Vote.ts | 10 +++++++++ 30 files changed, 250 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74376d5e9b..b24dfcf8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes: - `{BTree|Hash}Map.toHuman()` now display `Raw` keys as ascii (if detected) +- Add `toPrimitive()` to all base `Codec` interfaces (string/boolean/number) ## 8.13.1 Jul 16, 2022 diff --git a/packages/types-codec/src/abstract/Array.ts b/packages/types-codec/src/abstract/Array.ts index fd85ab65c9..634a0f432c 100644 --- a/packages/types-codec/src/abstract/Array.ts +++ b/packages/types-codec/src/abstract/Array.ts @@ -140,6 +140,19 @@ export abstract class AbstractArray extends Array implements return result; } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): AnyJson { + const result = new Array(this.length); + + for (let i = 0; i < this.length; i++) { + result[i] = this[i] && this[i].toPrimitive(); + } + + return result; + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/abstract/Base.ts b/packages/types-codec/src/abstract/Base.ts index 09c5dc2bd6..ea5c15adda 100644 --- a/packages/types-codec/src/abstract/Base.ts +++ b/packages/types-codec/src/abstract/Base.ts @@ -83,6 +83,13 @@ export abstract class AbstractBase implements Codec { return this.#raw.toJSON(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): AnyJson { + return this.#raw.toPrimitive(); + } + /** * @description Returns the string representation of the value */ diff --git a/packages/types-codec/src/abstract/Int.ts b/packages/types-codec/src/abstract/Int.ts index 06e7c80f95..1278c254ea 100644 --- a/packages/types-codec/src/abstract/Int.ts +++ b/packages/types-codec/src/abstract/Int.ts @@ -207,6 +207,15 @@ export abstract class AbstractInt extends BN implements INumber { : 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 */ diff --git a/packages/types-codec/src/base/Compact.ts b/packages/types-codec/src/base/Compact.ts index 13399578a6..a48267bcdc 100644 --- a/packages/types-codec/src/base/Compact.ts +++ b/packages/types-codec/src/base/Compact.ts @@ -171,6 +171,13 @@ export class Compact implements ICompact { return this.#raw.toNumber(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): string | number { + return this.#raw.toPrimitive(); + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/base/DoNotConstruct.ts b/packages/types-codec/src/base/DoNotConstruct.ts index 39d9c8396f..eee7763c80 100644 --- a/packages/types-codec/src/base/DoNotConstruct.ts +++ b/packages/types-codec/src/base/DoNotConstruct.ts @@ -72,6 +72,10 @@ export class DoNotConstruct implements Codec { throw this.#neverError; } + toPrimitive (): AnyJson { + throw this.#neverError; + } + toRawType (): string { throw this.#neverError; } diff --git a/packages/types-codec/src/base/Enum.ts b/packages/types-codec/src/base/Enum.ts index fc5d60201d..c628185785 100644 --- a/packages/types-codec/src/base/Enum.ts +++ b/packages/types-codec/src/base/Enum.ts @@ -414,6 +414,15 @@ export class Enum implements IEnum { return this.index; } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): AnyJson { + return this.#isBasic + ? this.type + : { [stringCamelCase(this.type)]: this.#raw.toPrimitive() }; + } + /** * @description Returns a raw struct representation of the enum types */ diff --git a/packages/types-codec/src/base/Null.ts b/packages/types-codec/src/base/Null.ts index 8bd4af2922..527f291d47 100644 --- a/packages/types-codec/src/base/Null.ts +++ b/packages/types-codec/src/base/Null.ts @@ -69,6 +69,13 @@ export class Null implements Codec { 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 */ diff --git a/packages/types-codec/src/base/Option.ts b/packages/types-codec/src/base/Option.ts index 167714bf2d..cc66e99346 100644 --- a/packages/types-codec/src/base/Option.ts +++ b/packages/types-codec/src/base/Option.ts @@ -206,6 +206,15 @@ export class Option implements IOption { : this.#raw.toJSON(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): AnyJson { + return this.isNone + ? null + : this.#raw.toPrimitive(); + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/extended/BTreeSet.ts b/packages/types-codec/src/extended/BTreeSet.ts index 19c721cbab..9c2e097e4b 100644 --- a/packages/types-codec/src/extended/BTreeSet.ts +++ b/packages/types-codec/src/extended/BTreeSet.ts @@ -193,6 +193,19 @@ export class BTreeSet extends Set implements ISet 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 (): AnyJson { + const json: AnyJson = []; + + for (const v of this.values()) { + json.push(v.toPrimitive()); + } + + return json; + } + /** * @description Returns the string representation of the value */ diff --git a/packages/types-codec/src/extended/Map.ts b/packages/types-codec/src/extended/Map.ts index 9fa8bb3205..3272e5d153 100644 --- a/packages/types-codec/src/extended/Map.ts +++ b/packages/types-codec/src/extended/Map.ts @@ -206,6 +206,23 @@ export class CodecMap extends return json; } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): AnyJson { + const json: Record = {}; + + for (const [k, v] of this.entries()) { + json[ + k instanceof Raw && k.isAscii + ? k.toUtf8() + : k.toString() + ] = v.toPrimitive(); + } + + return json; + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/extended/WrapperKeepOpaque.ts b/packages/types-codec/src/extended/WrapperKeepOpaque.ts index 6f7d1db36a..4cd3c164cf 100644 --- a/packages/types-codec/src/extended/WrapperKeepOpaque.ts +++ b/packages/types-codec/src/extended/WrapperKeepOpaque.ts @@ -92,6 +92,15 @@ export class WrapperKeepOpaque extends Bytes { : super.toHuman(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): any { + return this.#decoded + ? this.#decoded.toPrimitive() + : super.toPrimitive(); + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/native/Bool.ts b/packages/types-codec/src/native/Bool.ts index 80ed54da98..6fec1d767c 100644 --- a/packages/types-codec/src/native/Bool.ts +++ b/packages/types-codec/src/native/Bool.ts @@ -105,6 +105,13 @@ export class bool extends Boolean implements Codec { 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 */ diff --git a/packages/types-codec/src/native/Date.ts b/packages/types-codec/src/native/Date.ts index 0503a46a3b..2337560796 100644 --- a/packages/types-codec/src/native/Date.ts +++ b/packages/types-codec/src/native/Date.ts @@ -136,6 +136,13 @@ export class CodecDate extends Date implements INumber { 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 */ diff --git a/packages/types-codec/src/native/Float.ts b/packages/types-codec/src/native/Float.ts index 88f8804e77..f0c1a2e961 100644 --- a/packages/types-codec/src/native/Float.ts +++ b/packages/types-codec/src/native/Float.ts @@ -112,6 +112,13 @@ export class Float extends Number implements IFloat { 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 */ diff --git a/packages/types-codec/src/native/Json.ts b/packages/types-codec/src/native/Json.ts index 9350d72c7a..67fa8665b5 100644 --- a/packages/types-codec/src/native/Json.ts +++ b/packages/types-codec/src/native/Json.ts @@ -109,6 +109,19 @@ export class Json extends Map implements Codec { }, {}); } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): Record { + return [...this.entries()].reduce>((json, [key, value]): Record => { + json[key] = isFunction((value as Codec).toHuman) + ? (value as Codec).toPrimitive() + : value as AnyJson; + + return json; + }, {}); + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/native/Raw.spec.ts b/packages/types-codec/src/native/Raw.spec.ts index e86ac911f3..bfa7385144 100644 --- a/packages/types-codec/src/native/Raw.spec.ts +++ b/packages/types-codec/src/native/Raw.spec.ts @@ -36,6 +36,7 @@ describe('Raw', (): void => { testEncode('toJSON', '0x0102030405'); testEncode('toHex', '0x0102030405'); + testEncode('toPrimitive', '0x0102030405'); testEncode('toString', '0x0102030405'); testEncode('toU8a', Uint8Array.from([1, 2, 3, 4, 5])); @@ -79,6 +80,11 @@ describe('Raw', (): void => { expect(new Raw(registry, '0x2021222324').isAscii).toBe(true); }); + 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('你好'); diff --git a/packages/types-codec/src/native/Raw.ts b/packages/types-codec/src/native/Raw.ts index 7948d4d27e..548ce5320c 100644 --- a/packages/types-codec/src/native/Raw.ts +++ b/packages/types-codec/src/native/Raw.ts @@ -111,6 +111,20 @@ export class Raw extends Uint8Array implements IU8a { * @description Converts the Object to to a human-friendly JSON, with additional fields, expansion and formatting of information */ public toHuman (): AnyJson { + return this.toPrimitive(); + } + + /** + * @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 (): AnyJson { if (this.isAscii) { const text = this.toUtf8(); @@ -123,13 +137,6 @@ export class Raw extends Uint8Array implements IU8a { return this.toJSON(); } - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): string { - return this.toHex(); - } - /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/native/Set.ts b/packages/types-codec/src/native/Set.ts index de375bcead..197caa86fe 100644 --- a/packages/types-codec/src/native/Set.ts +++ b/packages/types-codec/src/native/Set.ts @@ -229,6 +229,13 @@ export class CodecSet extends Set implements ISet { 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 */ diff --git a/packages/types-codec/src/native/Struct.ts b/packages/types-codec/src/native/Struct.ts index b682cd42d4..3eca4c1b2a 100644 --- a/packages/types-codec/src/native/Struct.ts +++ b/packages/types-codec/src/native/Struct.ts @@ -293,6 +293,23 @@ export class Struct< return json; } + /** + * @description Converts the value in a best-fit primitive form + */ + public toPrimitive (): Record { + const json: Record = {}; + + for (const [k, v] of this.entries()) { + const jsonKey = this.#jsonMap.get(k) || k; + + // 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) + json[jsonKey as string] = v && v.toPrimitive(); + } + + return json; + } + /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types-codec/src/native/Text.ts b/packages/types-codec/src/native/Text.ts index 5b0283d4bd..ed876f3f77 100644 --- a/packages/types-codec/src/native/Text.ts +++ b/packages/types-codec/src/native/Text.ts @@ -147,6 +147,13 @@ export class Text extends String implements IText { 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 */ diff --git a/packages/types-codec/src/types/codec.ts b/packages/types-codec/src/types/codec.ts index fea96002ce..8d6fa2d061 100644 --- a/packages/types-codec/src/types/codec.ts +++ b/packages/types-codec/src/types/codec.ts @@ -77,6 +77,11 @@ export interface Codec { */ toJSON (): AnyJson; + /** + * @description Converts the value in a best-fit primitive form + */ + toPrimitive (): AnyJson; + /** * @description Returns the base runtime type name for this instance */ @@ -105,6 +110,6 @@ export interface CodecClass { new(registry: Registry, ...args: any[]): T; } -export type CodecTo = 'toHex' | 'toJSON' | 'toString' | 'toU8a'; +export type CodecTo = 'toHex' | 'toJSON' | 'toPrimitive' | 'toString' | 'toU8a'; export type ArgsDef = Record; diff --git a/packages/types-codec/src/types/interfaces.ts b/packages/types-codec/src/types/interfaces.ts index cc0b30cb3c..8bb4d71d98 100644 --- a/packages/types-codec/src/types/interfaces.ts +++ b/packages/types-codec/src/types/interfaces.ts @@ -31,6 +31,7 @@ export interface INumber extends Codec { toBigInt (): bigint; toBn (): BN; toNumber (): number; + toPrimitive (): string | number; } export interface IFloat extends Codec { diff --git a/packages/types/src/ethereum/AccountId.ts b/packages/types/src/ethereum/AccountId.ts index 3e25c7e734..703bdacefb 100644 --- a/packages/types/src/ethereum/AccountId.ts +++ b/packages/types/src/ethereum/AccountId.ts @@ -53,6 +53,13 @@ export class GenericEthereumAccountId extends U8aFixed { return this.toString(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): string { + return this.toJSON(); + } + /** * @description Returns the string representation of the value */ diff --git a/packages/types/src/extrinsic/Extrinsic.spec.ts b/packages/types/src/extrinsic/Extrinsic.spec.ts index 5cec0c3602..638ac63d21 100644 --- a/packages/types/src/extrinsic/Extrinsic.spec.ts +++ b/packages/types/src/extrinsic/Extrinsic.spec.ts @@ -43,6 +43,7 @@ describe('Extrinsic', (): void => { expect(extrinsic.callIndex).toEqual(new Uint8Array([6, 0])); expect(extrinsic.args[0].toHex()).toEqual('0x00495e1e506f266418af07fa0c5c108dd436f2faa59fe7d9e54403779f5bbd7718'); expect(extrinsic.args[1].toHuman()).toEqual('104,560,923,320,000'); // ('104.5609 Unit'); + expect(extrinsic.toPrimitive()).toEqual({ method: { args: { dest: { id: '5DiuK2zR4asj2CEh77SKtUgTswTLkD8eiAKrByg5G3wL5w9b' }, value: 104560923320000 }, callIndex: '0x0600' }, signature: { era: { mortalEra: [1024, 186] }, nonce: 68, signature: { ed25519: '0xd99ffe3e610ad234e1414bda5831395a6df9098bf80b01561ce89a5065ae89d5c10e1619c6c99131b0bea4fb73ef04d07c07770e2ae9df5c325c331769ccb300' }, signer: { id: '5Hn8KKEp8qruCGWaN9MEsjTs4FXB4wv9xn7g1RWkNeKKNXCr' }, tip: 30000000000 } }); }); }); }); diff --git a/packages/types/src/generic/AccountId.ts b/packages/types/src/generic/AccountId.ts index 57a228672f..57ff045d29 100644 --- a/packages/types/src/generic/AccountId.ts +++ b/packages/types/src/generic/AccountId.ts @@ -62,6 +62,13 @@ export class GenericAccountId extends U8aFixed { return this.toString(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): string { + return this.toJSON(); + } + /** * @description Returns the string representation of the value */ diff --git a/packages/types/src/generic/AccountIndex.ts b/packages/types/src/generic/AccountIndex.ts index 782d0671dd..6ff6fb8126 100644 --- a/packages/types/src/generic/AccountIndex.ts +++ b/packages/types/src/generic/AccountIndex.ts @@ -104,6 +104,13 @@ export class GenericAccountIndex extends u32 { return this.toString(); } + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): string { + return this.toJSON(); + } + /** * @description Returns the string representation of the value */ diff --git a/packages/types/src/generic/Call.ts b/packages/types/src/generic/Call.ts index b8870ab320..c7ad855bf0 100644 --- a/packages/types/src/generic/Call.ts +++ b/packages/types/src/generic/Call.ts @@ -104,6 +104,13 @@ export class GenericCallIndex extends U8aFixed { constructor (registry: Registry, value?: AnyU8a) { super(registry, value, 16); } + + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): string { + return this.toHex(); + } } /** diff --git a/packages/types/src/generic/Vote.spec.ts b/packages/types/src/generic/Vote.spec.ts index 35ae3b2b3c..79936b61c7 100644 --- a/packages/types/src/generic/Vote.spec.ts +++ b/packages/types/src/generic/Vote.spec.ts @@ -7,7 +7,7 @@ import { GenericVote } from '.'; describe('GenericVote', (): void => { const registry = new TypeRegistry(); - describe.only('construction', (): void => { + describe('construction', (): void => { it('constructs via boolean true', (): void => { expect(new GenericVote(registry, true).toU8a()).toEqual(new Uint8Array([128])); expect(new GenericVote(registry, true).isAye).toBe(true); @@ -58,7 +58,7 @@ describe('GenericVote', (): void => { expect(new GenericVote(registry, new Uint8Array([1])).isNay).toBe(true); }); - it.only('constructs via Uint8Array (aye)', (): void => { + it('constructs via Uint8Array (aye)', (): void => { const test = new GenericVote(registry, new Uint8Array([0b10000010])); expect(test.isNay).toBe(false); @@ -68,12 +68,20 @@ describe('GenericVote', (): void => { describe('Vote with conviction', (): void => { it('constructs Vote with raw boolean', (): void => { + const vote = new GenericVote(registry, { + aye: true, + conviction: 'Locked1x' + }); + expect( - new GenericVote(registry, { - aye: true, - conviction: 'Locked1x' - }).toU8a() + vote.toU8a() ).toEqual(new Uint8Array([0b10000001])); + expect( + vote.toPrimitive() + ).toEqual({ + aye: true, + conviction: 'Locked1x' + }); }); it('constructs with Vote aye is false, conviction is None', (): void => { diff --git a/packages/types/src/generic/Vote.ts b/packages/types/src/generic/Vote.ts index c04cb0bd76..c347bffb97 100644 --- a/packages/types/src/generic/Vote.ts +++ b/packages/types/src/generic/Vote.ts @@ -115,6 +115,16 @@ export class GenericVote extends U8aFixed { }; } + /** + * @description Converts the value in a best-fit primitive form + */ + public override toPrimitive (): any { + return { + aye: this.isAye, + conviction: this.conviction.toPrimitive() + }; + } + /** * @description Returns the base runtime type name for this instance */