From 1df3fd3d9260eb225319b25c505dfc2cfd9f95b3 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:02:23 -0700 Subject: [PATCH] [TS-SDK v2] Updating the `Deserializable` interface and making `Serializable` an abstract class (#10307) * Removing export from Deserializable to facilitate using a static `deserialize` method, fixing error messages in unit tests for multi_ed25519, and changing Serializable to an abstract class that implements `bcsToBytes()`. Removed abstract deserialize from public/private key classes. * Re-adding doc comments * Adding `serialize` and `deserialize` and corresponding unit tests to the AccountAddress class * Fixing multi_ed25519 error messages * Updating doc comments for Deserializable to clarify what its purpose is. --- .../typescript/sdk_v2/src/bcs/deserializer.ts | 77 ++++--------------- .../typescript/sdk_v2/src/bcs/serializer.ts | 22 +++++- .../sdk_v2/src/core/account_address.ts | 34 +++++++- .../sdk_v2/src/crypto/asymmetric_crypto.ts | 17 +--- .../typescript/sdk_v2/src/crypto/ed25519.ts | 15 ---- .../sdk_v2/src/crypto/multi_ed25519.ts | 10 --- .../sdk_v2/tests/unit/account_address.test.ts | 56 ++++++++++++++ .../sdk_v2/tests/unit/deserializer.test.ts | 38 ++++----- .../sdk_v2/tests/unit/serializer.test.ts | 12 ++- 9 files changed, 150 insertions(+), 131 deletions(-) diff --git a/ecosystem/typescript/sdk_v2/src/bcs/deserializer.ts b/ecosystem/typescript/sdk_v2/src/bcs/deserializer.ts index 84c18fdb4617a..9ebe3745092ea 100644 --- a/ecosystem/typescript/sdk_v2/src/bcs/deserializer.ts +++ b/ecosystem/typescript/sdk_v2/src/bcs/deserializer.ts @@ -5,7 +5,12 @@ import { MAX_U32_NUMBER } from "./consts"; import { Uint128, Uint16, Uint256, Uint32, Uint64, Uint8 } from "../types"; -export interface Deserializable { +/** + * This interface exists solely for the `deserialize` function in the `Deserializer` class. + * It is not exported because exporting it results in more typing errors than it prevents + * due to Typescript's lack of support for static methods in abstract classes and interfaces. + */ +interface Deserializable { deserialize(deserializer: Deserializer): T; } @@ -190,71 +195,21 @@ export class Deserializer { } /** - * This function deserializes a Deserializable value. The bytes must be loaded into the Serializer already. - * Note that it does not take in the value, it takes in the class type of the value that implements Serializable. + * Helper function that primarily exists to support alternative syntax for deserialization. + * That is, if we have a `const deserializer: new Deserializer(...)`, instead of having to use + * `MyClass.deserialize(deserializer)`, we can call `deserializer.deserialize(MyClass)`. * - * The process of using this function is as follows: - * 1. Serialize the value of class type T using its `serialize` function. - * 2. Get the serialized bytes and pass them into the Deserializer constructor. - * 3. Call this function with your newly constructed Deserializer, as `deserializer.deserialize(ClassType)` - * - * @param cls The Deserializable class to deserialize the buffered bytes into. - * - * @example - * // Define the MoveStruct class that implements the Deserializable interface - * class MoveStruct implements Serializable { - * constructor( - * public name: string, - * public description: string, - * public enabled: boolean, - * public vectorU8: Array, - * ) {} - * - * serialize(serializer: Serializer): void { - * serializer.serializeStr(this.name); - * serializer.serializeStr(this.description); - * serializer.serializeBool(this.enabled); - * serializer.serializeU32AsUleb128(this.vectorU8.length); - * this.vectorU8.forEach((n) => serializer.serializeU8(n)); - * } - * - * static deserialize(deserializer: Deserializer): MoveStruct { - * const name = deserializer.deserializeStr(); - * const description = deserializer.deserializeStr(); - * const enabled = deserializer.deserializeBool(); - * const length = deserializer.deserializeUleb128AsU32(); - * const vectorU8 = new Array(); - * for (let i = 0; i < length; i++) { - * vectorU8.push(deserializer.deserializeU8()); - * } - * return new MoveStruct(name, description, enabled, vectorU8); - * } - * } - * - * // Construct a MoveStruct - * const moveStruct = new MoveStruct("abc", "123", false, [1, 2, 3, 4]); - * - * // Serialize a MoveStruct instance. - * const serializer = new Serializer(); - * serializer.serialize(moveStruct); - * const moveStructBcsBytes = serializer.toUint8Array(); - * - * // Load the bytes into the Deserializer buffer - * const deserializer = new Deserializer(moveStructBcsBytes); - * - * // Deserialize the buffered bytes into an instance of MoveStruct - * const deserializedMoveStruct = deserializer.deserialize(MoveStruct); - * assert(deserializedMoveStruct.name === moveStruct.name); - * assert(deserializedMoveStruct.description === moveStruct.description); - * assert(deserializedMoveStruct.enabled === moveStruct.enabled); - * assert(deserializedMoveStruct.vectorU8.length === moveStruct.vectorU8.length); - * deserializeMoveStruct.vectorU8.forEach((n, i) => assert(n === moveStruct.vectorU8[i])); + * @example const deserializer = new Deserializer(new Uint8Array([1, 2, 3])); + * const value = deserializer.deserialize(MyClass); // where MyClass has a `deserialize` function + * // value is now an instance of MyClass + * // equivalent to `const value = MyClass.deserialize(deserializer)` + * @param cls The BCS-deserializable class to deserialize the buffered bytes into. * * @returns the deserialized value of class type T */ deserialize(cls: Deserializable): T { - // NOTE: The `deserialize` method called by `cls` is defined in the `cls`'s - // Deserializable interface, not the one defined in this class. + // NOTE: `deserialize` in `cls.deserialize(this)` here is a static method defined in `cls`, + // It is separate from the `deserialize` instance method defined here in Deserializer. return cls.deserialize(this); } } diff --git a/ecosystem/typescript/sdk_v2/src/bcs/serializer.ts b/ecosystem/typescript/sdk_v2/src/bcs/serializer.ts index 9addad26fc127..ad6a322e7798e 100644 --- a/ecosystem/typescript/sdk_v2/src/bcs/serializer.ts +++ b/ecosystem/typescript/sdk_v2/src/bcs/serializer.ts @@ -12,8 +12,22 @@ import { } from "./consts"; import { AnyNumber, Uint16, Uint32, Uint8 } from "../types"; -export interface Serializable { - serialize(serializer: Serializer): void; +// This class is intended to be used as a base class for all serializable types. +// It can be used to facilitate composable serialization of a complex type and +// in general to serialize a type to its BCS representation. +export abstract class Serializable { + abstract serialize(serializer: Serializer): void; + + /** + * Serializes a `Serializable` value to its BCS representation. + * This function is the Typescript SDK equivalent of `bcs::to_bytes` in Move. + * @returns the BCS representation of the Serializable instance as a byte buffer + */ + bcsToBytes(): Uint8Array { + const serializer = new Serializer(); + this.serialize(serializer); + return serializer.toUint8Array(); + } } export class Serializer { @@ -235,9 +249,9 @@ export class Serializer { * * @example * // Define the MoveStruct class that implements the Serializable interface - * class MoveStruct implements Serializable { + * class MoveStruct extends Serializable { * constructor( - * public creatorAddress: AccountAddress, // where AccountAddress implements Serializable + * public creatorAddress: AccountAddress, // where AccountAddress extends Serializable * public collectionName: string, * public tokenName: string * ) {} diff --git a/ecosystem/typescript/sdk_v2/src/core/account_address.ts b/ecosystem/typescript/sdk_v2/src/core/account_address.ts index 4e2c72e30d533..61301977fb395 100644 --- a/ecosystem/typescript/sdk_v2/src/core/account_address.ts +++ b/ecosystem/typescript/sdk_v2/src/core/account_address.ts @@ -4,6 +4,7 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { HexInput } from "../types"; import { ParsingError, ParsingResult } from "./common"; +import { Deserializer, Serializable, Serializer } from "../bcs"; /** * This enum is used to explain why an address was invalid. @@ -34,7 +35,7 @@ export enum AddressInvalidReason { * The comments in this class make frequent reference to the LONG and SHORT formats, * as well as "special" addresses. To learn what these refer to see AIP-40. */ -export class AccountAddress { +export class AccountAddress extends Serializable { /* * This is the internal representation of an account address. */ @@ -64,6 +65,7 @@ export class AccountAddress { * @param args.data A Uint8Array representing an account address. */ constructor(args: { data: Uint8Array }) { + super(); if (args.data.length !== AccountAddress.LENGTH) { throw new ParsingError( "AccountAddress data should be exactly 32 bytes long", @@ -164,6 +166,36 @@ export class AccountAddress { return this.data; } + /** + * Serialize the AccountAddress to a Serializer instance's data buffer. + * @param serializer The serializer to serialize the AccountAddress to. + * @returns void + * @example + * const serializer = new Serializer(); + * const address = AccountAddress.fromString({ input: "0x1" }); + * address.serialize(serializer); + * const bytes = serializer.toUint8Array(); + * // `bytes` is now the BCS-serialized address. + */ + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.data); + } + + /** + * Deserialize an AccountAddress from the byte buffer in a Deserializer instance. + * @param deserializer The deserializer to deserialize the AccountAddress from. + * @returns An instance of AccountAddress. + * @example + * const bytes = hexToBytes("0x0102030405060708091011121314151617181920212223242526272829303132"); + * const deserializer = new Deserializer(bytes); + * const address = AccountAddress.deserialize(deserializer); + * // `address` is now an instance of AccountAddress. + */ + static deserialize(deserializer: Deserializer): AccountAddress { + const bytes = deserializer.deserializeFixedBytes(AccountAddress.LENGTH); + return new AccountAddress({ data: bytes }); + } + // === // Methods for creating an instance of AccountAddress from other types. // === diff --git a/ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts b/ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts index 62223a20c6c98..8812b26cb8328 100644 --- a/ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts +++ b/ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts @@ -1,14 +1,14 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -import { Deserializable, Deserializer, Serializable, Serializer } from "../bcs"; +import { Serializable, Serializer } from "../bcs"; import { HexInput } from "../types"; /** * An abstract representation of a public key. All Asymmetric key pairs will use this to * verify signatures and for authentication keys. */ -export abstract class PublicKey implements Serializable, Deserializable { +export abstract class PublicKey extends Serializable { /** * Verifies that the private key associated with this public key signed the message with the given signature. * @param args @@ -25,9 +25,6 @@ export abstract class PublicKey implements Serializable, Deserializable { +export abstract class PrivateKey extends Serializable { /** * Sign a message with the key * @param args @@ -52,9 +49,6 @@ export abstract class PrivateKey implements Serializable, Deserializable { +export abstract class Signature extends Serializable { /** * Get the raw signature bytes */ @@ -78,8 +72,5 @@ export abstract class Signature implements Serializable, Deserializable { expect(addressOne.equals(addressTwo)).toBeTruthy(); }); }); + +describe("AccountAddress serialization and deserialization", () => { + const serializeAndCheckEquality = (address: AccountAddress) => { + const serializer = new Serializer(); + serializer.serialize(address); + expect(serializer.toUint8Array()).toEqual(address.toUint8Array()); + expect(serializer.toUint8Array()).toEqual(address.bcsToBytes()); + }; + + it("serializes an unpadded, full, and reserved address correctly", () => { + const address1 = AccountAddress.fromStringRelaxed({ input: "0x0102030a0b0c" }); + const address2 = AccountAddress.fromStringRelaxed({ input: ADDRESS_OTHER.longWith0x }); + const address3 = AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.shortWithout0x }); + serializeAndCheckEquality(address1); + serializeAndCheckEquality(address2); + serializeAndCheckEquality(address3); + }); + + it("deserializes a byte buffer into an address correctly", () => { + const bytes = ADDRESS_TEN.bytes; + const deserializer = new Deserializer(bytes); + const deserializedAddress = AccountAddress.deserialize(deserializer); + expect(deserializedAddress.toUint8Array()).toEqual(bytes); + }); + + it("deserializes an unpadded, full, and reserved address correctly", () => { + const serializer = new Serializer(); + const address1 = AccountAddress.fromStringRelaxed({ input: "0x123abc" }); + const address2 = AccountAddress.fromStringRelaxed({ input: ADDRESS_OTHER.longWith0x }); + const address3 = AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.shortWithout0x }); + serializer.serialize(address1); + serializer.serialize(address2); + serializer.serialize(address3); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedAddress1 = AccountAddress.deserialize(deserializer); + const deserializedAddress2 = AccountAddress.deserialize(deserializer); + const deserializedAddress3 = AccountAddress.deserialize(deserializer); + expect(deserializedAddress1.toUint8Array()).toEqual(address1.toUint8Array()); + expect(deserializedAddress2.toUint8Array()).toEqual(address2.toUint8Array()); + expect(deserializedAddress3.toUint8Array()).toEqual(address3.toUint8Array()); + }); + + it("serializes and deserializes an address correctly", () => { + const address = AccountAddress.fromStringRelaxed({ input: "0x0102030a0b0c" }); + const serializer = new Serializer(); + serializer.serialize(address); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedAddress = AccountAddress.deserialize(deserializer); + expect(deserializedAddress.toUint8Array()).toEqual(address.toUint8Array()); + const bytes = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 10, 11, 12, + ]); + expect(deserializedAddress.toUint8Array()).toEqual(bytes); + }); +}); diff --git a/ecosystem/typescript/sdk_v2/tests/unit/deserializer.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/deserializer.test.ts index 2a13b0379c4a0..67d70813e0acc 100644 --- a/ecosystem/typescript/sdk_v2/tests/unit/deserializer.test.ts +++ b/ecosystem/typescript/sdk_v2/tests/unit/deserializer.test.ts @@ -1,7 +1,7 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -import { Serializable, Serializer, Deserializer, Deserializable } from "../../src/bcs"; +import { Serializable, Serializer, Deserializer } from "../../src/bcs"; describe("BCS Deserializer", () => { it("deserializes a non-empty string", () => { @@ -132,13 +132,15 @@ describe("BCS Deserializer", () => { it("deserializes a single deserializable class", () => { // Define the MoveStruct class that implements the Deserializable interface - class MoveStruct implements Serializable { + class MoveStruct extends Serializable { constructor( public name: string, public description: string, public enabled: boolean, public vectorU8: Array, - ) {} + ) { + super(); + } serialize(serializer: Serializer): void { serializer.serializeStr(this.name); @@ -169,7 +171,7 @@ describe("BCS Deserializer", () => { // Load the bytes into the Deserializer buffer const deserializer = new Deserializer(moveStructBcsBytes); // Deserialize the buffered bytes into an instance of MoveStruct - const deserializedMoveStruct = deserializer.deserialize(MoveStruct); + const deserializedMoveStruct = MoveStruct.deserialize(deserializer); expect(deserializedMoveStruct.name).toEqual(moveStruct.name); expect(deserializedMoveStruct.description).toEqual(moveStruct.description); expect(deserializedMoveStruct.enabled).toEqual(moveStruct.enabled); @@ -177,7 +179,7 @@ describe("BCS Deserializer", () => { }); it("deserializes and composes an abstract Deserializable class instance from composed deserialize calls", () => { - abstract class MoveStruct { + abstract class MoveStruct extends Serializable { abstract serialize(serializer: Serializer): void; static deserialize(deserializer: Deserializer): MoveStruct { @@ -193,27 +195,15 @@ describe("BCS Deserializer", () => { } } - class MoveStructs implements Serializable { - constructor(public moveStruct1: MoveStruct, public moveStruct2: MoveStruct) {} - - serialize(serializer: Serializer): void { - serializer.serialize(this.moveStruct1); - serializer.serialize(this.moveStruct2); - } - - // deserialize two MoveStructs, potentially either MoveStructA or MoveStructB - static deserialize(deserializer: Deserializer): MoveStructs { - return new MoveStructs(MoveStruct.deserialize(deserializer), MoveStruct.deserialize(deserializer)); - } - } - - class MoveStructA implements Serializable { + class MoveStructA extends Serializable { constructor( public name: string, public description: string, public enabled: boolean, public vectorU8: Array, - ) {} + ) { + super(); + } serialize(serializer: Serializer): void { // enum variant index for the abstract MoveStruct class @@ -237,13 +227,15 @@ describe("BCS Deserializer", () => { return new MoveStructA(name, description, enabled, vectorU8); } } - class MoveStructB implements Serializable { + class MoveStructB extends Serializable { constructor( public moveStructA: MoveStructA, public name: string, public description: string, public vectorU8: Array, - ) {} + ) { + super(); + } serialize(serializer: Serializer): void { // enum variant index for the abstract MoveStruct class diff --git a/ecosystem/typescript/sdk_v2/tests/unit/serializer.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/serializer.test.ts index b8b1b0793e181..f9832146efecc 100644 --- a/ecosystem/typescript/sdk_v2/tests/unit/serializer.test.ts +++ b/ecosystem/typescript/sdk_v2/tests/unit/serializer.test.ts @@ -225,13 +225,15 @@ describe("BCS Serializer", () => { }); it("serializes multiple Serializable values", () => { - class MoveStructA implements Serializable { + class MoveStructA extends Serializable { constructor( public name: string, public description: string, public enabled: boolean, public vectorU8: Array, - ) {} + ) { + super(); + } serialize(serializer: Serializer): void { serializer.serializeStr(this.name); @@ -241,13 +243,15 @@ describe("BCS Serializer", () => { this.vectorU8.forEach((n) => serializer.serializeU8(n)); } } - class MoveStructB implements Serializable { + class MoveStructB extends Serializable { constructor( public moveStructA: MoveStructA, public name: string, public description: string, public vectorU8: Array, - ) {} + ) { + super(); + } serialize(serializer: Serializer): void { serializer.serialize(this.moveStructA);