Skip to content

Commit

Permalink
[TS-SDK v2] Updating the Deserializable interface and making `Seria…
Browse files Browse the repository at this point in the history
…lizable` an abstract class (aptos-labs#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.
  • Loading branch information
xbtmatt authored and Zekun Wang committed Oct 13, 2023
1 parent 2fb8c11 commit c3b8a41
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 131 deletions.
77 changes: 16 additions & 61 deletions ecosystem/typescript/sdk_v2/src/bcs/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import { MAX_U32_NUMBER } from "./consts";
import { Uint128, Uint16, Uint256, Uint32, Uint64, Uint8 } from "../types";

export interface Deserializable<T> {
/**
* 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<T> {
deserialize(deserializer: Deserializer): T;
}

Expand Down Expand Up @@ -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<number>,
* ) {}
*
* 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<number>();
* 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<T>(cls: Deserializable<T>): 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);
}
}
22 changes: 18 additions & 4 deletions ecosystem/typescript/sdk_v2/src/bcs/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
* ) {}
Expand Down
34 changes: 33 additions & 1 deletion ecosystem/typescript/sdk_v2/src/core/account_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.
// ===
Expand Down
17 changes: 4 additions & 13 deletions ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts
Original file line number Diff line number Diff line change
@@ -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<PublicKey> {
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
Expand All @@ -25,17 +25,14 @@ export abstract class PublicKey implements Serializable, Deserializable<PublicKe
*/
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): PublicKey;

abstract serialize(serializer: Serializer): void;
}

/**
* An abstract representation of a private key. This is used to sign transactions and
* derive the public key associated.
*/
export abstract class PrivateKey implements Serializable, Deserializable<PrivateKey> {
export abstract class PrivateKey extends Serializable {
/**
* Sign a message with the key
* @param args
Expand All @@ -52,9 +49,6 @@ export abstract class PrivateKey implements Serializable, Deserializable<Private
*/
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): PrivateKey;

abstract serialize(serializer: Serializer): void;

/**
Expand All @@ -67,7 +61,7 @@ export abstract class PrivateKey implements Serializable, Deserializable<Private
* An abstract representation of a signature. This is the product of signing a
* message and can be used with the PublicKey to verify the signature.
*/
export abstract class Signature implements Serializable, Deserializable<Signature> {
export abstract class Signature extends Serializable {
/**
* Get the raw signature bytes
*/
Expand All @@ -78,8 +72,5 @@ export abstract class Signature implements Serializable, Deserializable<Signatur
*/
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): Signature;

abstract serialize(serializer: Serializer): void;
}
15 changes: 0 additions & 15 deletions ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ export class Ed25519PublicKey extends PublicKey {
const bytes = deserializer.deserializeBytes();
return new Ed25519PublicKey({ hexInput: bytes });
}

// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
deserialize(deserializer: Deserializer): PublicKey {
throw new Error("Not implemented");
}
}

/**
Expand Down Expand Up @@ -150,11 +145,6 @@ export class Ed25519PrivateKey extends PrivateKey {
serializer.serializeBytes(this.toUint8Array());
}

// TODO: Update this in interface to be static, then remove this method
deserialize(deserializer: Deserializer): Ed25519PrivateKey {
throw new Error("Method not implemented.");
}

static deserialize(deserializer: Deserializer): Ed25519PrivateKey {
const bytes = deserializer.deserializeBytes();
return new Ed25519PrivateKey({ hexInput: bytes });
Expand Down Expand Up @@ -223,11 +213,6 @@ export class Ed25519Signature extends Signature {
serializer.serializeBytes(this.data.toUint8Array());
}

// TODO: Update this in interface to be static, then remove this method
deserialize(deserializer: Deserializer): Ed25519Signature {
throw new Error("Method not implemented.");
}

static deserialize(deserializer: Deserializer): Ed25519Signature {
const bytes = deserializer.deserializeBytes();
return new Ed25519Signature({ hexInput: bytes });
Expand Down
10 changes: 0 additions & 10 deletions ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,6 @@ export class MultiEd25519PublicKey extends PublicKey {
serializer.serializeBytes(this.toUint8Array());
}

// TODO: Update this in interface to be static, then remove this method
deserialize(deserializer: Deserializer): PublicKey {
throw new Error("Method not implemented.");
}

static deserialize(deserializer: Deserializer): MultiEd25519PublicKey {
const bytes = deserializer.deserializeBytes();
const threshold = bytes[bytes.length - 1];
Expand Down Expand Up @@ -240,11 +235,6 @@ export class MultiEd25519Signature extends Signature {
serializer.serializeBytes(this.toUint8Array());
}

// TODO: Update this in interface to be static, then remove this method
deserialize(deserializer: Deserializer): Signature {
throw new Error("Method not implemented.");
}

static deserialize(deserializer: Deserializer): MultiEd25519Signature {
const bytes = deserializer.deserializeBytes();
const bitmap = bytes.subarray(bytes.length - 4);
Expand Down
56 changes: 56 additions & 0 deletions ecosystem/typescript/sdk_v2/tests/unit/account_address.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { Deserializer, Serializer } from "../../src/bcs";
import { AccountAddress, AddressInvalidReason } from "../../src/core/account_address";

type Addresses = {
Expand Down Expand Up @@ -356,3 +357,58 @@ describe("AccountAddress other parsing", () => {
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);
});
});
Loading

0 comments on commit c3b8a41

Please sign in to comment.