这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see! In this case, the implementation of the method deserialize won't be enforced for child classes?

I wonder if theres a way to just implement the deserialize method directly in abstract/base class. Then all child classes will auto inherit it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I looked into it and there's apparently a lot of hullabaloo about static methods in abstract classes, so the support there, particularly with typescript, is lacking.

I tried a few different methods to achieve a static deserialize method interface inherited from the abstract class but it came down to either being really janky or just not possible.

WRT what you're saying though, the problem with implementing the deserialize method in the base class (say, as an abstract factory) is that it must know ahead of time how to create the child classes. I don't know if this would be a problem, it depends on how we plan on implementing these abstract classes later.

For example here is the TransactionAuthenticator abstract factory pattern:

export abstract class TransactionAuthenticator {
  abstract serialize(serializer: Serializer): void;

  static deserialize(deserializer: Deserializer): TransactionAuthenticator {
    const index = deserializer.deserializeUleb128AsU32();
    switch (index) {
      case 0:
        return TransactionAuthenticatorEd25519.load(deserializer);
      case 1:
        return TransactionAuthenticatorMultiEd25519.load(deserializer);
      case 2:
        return TransactionAuthenticatorMultiAgent.load(deserializer);
      case 3:
        return TransactionAuthenticatorFeePayer.load(deserializer);
      default:
        throw new Error(`Unknown variant index for TransactionAuthenticator: ${index}`);
    }
  }
}

But with this, the child classes actually don't even implement deserialize, they specifically have a load function they use.

The other part to this is that these are specifically enums in Rust, so that's why they're serialized/deserialized like this.

With the PublicKey stuff, it's not an enum in rust, it's merely an abstract class we're using for the SDK, so we wouldn't be serializing an enum value (0, 1, etc) at the beginning of the byte buffer to differentiate between child class implementations.

/**
* 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