From c45f7fc94500939e6f5a3a2ae5def5b2bd36083d Mon Sep 17 00:00:00 2001
From: "Daniel Porteous (dport)" <daniel@dport.me>
Date: Sat, 12 Aug 2023 00:28:38 +0100
Subject: [PATCH] [TS SDK v2] Add AccountAddress (#9564)

---
 .../sdk_v2/src/core/account_address.ts        | 340 ++++++++++++++++++
 ecosystem/typescript/sdk_v2/src/core/index.ts |   1 +
 .../sdk_v2/tests/unit/account_address.test.ts | 329 +++++++++++++++++
 3 files changed, 670 insertions(+)
 create mode 100644 ecosystem/typescript/sdk_v2/src/core/account_address.ts
 create mode 100644 ecosystem/typescript/sdk_v2/tests/unit/account_address.test.ts

diff --git a/ecosystem/typescript/sdk_v2/src/core/account_address.ts b/ecosystem/typescript/sdk_v2/src/core/account_address.ts
new file mode 100644
index 00000000000000..2436c5f41ebc88
--- /dev/null
+++ b/ecosystem/typescript/sdk_v2/src/core/account_address.ts
@@ -0,0 +1,340 @@
+// Copyright © Aptos Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
+import { HexInput } from "../types";
+import { ParsingError, ParsingResult } from "./common";
+
+/**
+ * This enum is used to explain why an address was invalid.
+ */
+export enum AddressInvalidReason {
+  INCORRECT_NUMBER_OF_BYTES = "incorrect_number_of_bytes",
+  INVALID_HEX_CHARS = "invalid_hex_chars",
+  TOO_SHORT = "too_short",
+  TOO_LONG = "too_long",
+  LEADING_ZERO_X_REQUIRED = "leading_zero_x_required",
+  LONG_FORM_REQUIRED_UNLESS_SPECIAL = "long_form_required_unless_special",
+}
+
+/**
+ * NOTE: Only use this class for account addresses. For other hex data, e.g. transaction
+ * hashes, use the Hex class.
+ *
+ * AccountAddress is used for working with account addresses. Account addresses, when
+ * represented as a string, generally look like these examples:
+ * - 0x1
+ * - 0xaa86fe99004361f747f91342ca13c426ca0cccb0c1217677180c9493bad6ef0c
+ *
+ * Proper formatting and parsing of account addresses is defined by AIP-40.
+ * To learn more about the standard, read the AIP here:
+ * https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
+ *
+ * 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 {
+  /*
+   * This is the internal representation of an account address.
+   */
+  readonly data: Uint8Array;
+
+  /*
+   * The number of bytes that make up an account address.
+   */
+  static readonly LENGTH: number = 32;
+
+  /*
+   * The length of an address string in LONG form without a leading 0x.
+   */
+  static readonly LONG_STRING_LENGTH: number = 64;
+
+  static ONE: AccountAddress = AccountAddress.fromString({ input: "0x1" });
+
+  static TWO: AccountAddress = AccountAddress.fromString({ input: "0x2" });
+
+  static THREE: AccountAddress = AccountAddress.fromString({ input: "0x3" });
+
+  static FOUR: AccountAddress = AccountAddress.fromString({ input: "0x4" });
+
+  /**
+   * Creates an instance of AccountAddress from a Uint8Array.
+   *
+   * @param args.data A Uint8Array representing an account address.
+   */
+  constructor(args: { data: Uint8Array }) {
+    if (args.data.length !== AccountAddress.LENGTH) {
+      throw new ParsingError(
+        "AccountAddress data should be exactly 32 bytes long",
+        AddressInvalidReason.INCORRECT_NUMBER_OF_BYTES,
+      );
+    }
+    this.data = args.data;
+  }
+
+  /**
+   * Returns whether an address is special, where special is defined as 0x0 to 0xf
+   * inclusive. In other words, the last byte of the address must be < 0b10000 (16)
+   * and every other byte must be zero.
+   *
+   * For more information on how special addresses are defined see AIP-40:
+   * https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
+   *
+   * @returns true if the address is special, false if not.
+   */
+  isSpecial(): boolean {
+    return (
+      this.data.slice(0, this.data.length - 1).every((byte) => byte === 0) && this.data[this.data.length - 1] < 0b10000
+    );
+  }
+
+  // ===
+  // Methods for representing an instance of AccountAddress as other types.
+  // ===
+
+  /**
+   * Return the AccountAddress as a string as per AIP-40.
+   * https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
+   *
+   * In short, it means that special addresses are represented in SHORT form, meaning
+   * 0x0 through to 0xf inclusive, and every other address is represented in LONG form,
+   * meaning 0x + 64 hex characters.
+   *
+   * @returns AccountAddress as a string conforming to AIP-40.
+   */
+  toString(): string {
+    return `0x${this.toStringWithoutPrefix()}`;
+  }
+
+  /**
+   * NOTE: Prefer to use `toString` where possible.
+   *
+   * Return the AccountAddress as a string as per AIP-40 but without the leading 0x.
+   *
+   * Learn more by reading the docstring of `toString`.
+   *
+   * @returns AccountAddress as a string conforming to AIP-40 but without the leading 0x.
+   */
+  toStringWithoutPrefix(): string {
+    let hex = bytesToHex(this.data);
+    if (this.isSpecial()) {
+      hex = hex[hex.length - 1];
+    }
+    return hex;
+  }
+
+  /**
+   * NOTE: Prefer to use `toString` where possible.
+   *
+   * Whereas toString will format special addresses (as defined by isSpecial) using the
+   * SHORT form (no leading 0s), this format the address in the LONG format
+   * unconditionally.
+   *
+   * This means it will be 0x + 64 hex characters.
+   *
+   * @returns AccountAddress as a string in LONG form.
+   */
+  toStringLong(): string {
+    return `0x${this.toStringLongWithoutPrefix()}`;
+  }
+
+  /*
+   * NOTE: Prefer to use `toString` where possible.
+   *
+   * Whereas toString will format special addresses (as defined by isSpecial) using the
+   * SHORT form (no leading 0s), this function will include leading zeroes. The string
+   * will not have a leading zero.
+   *
+   * This means it will be 64 hex characters without a leading 0x.
+   *
+   * @returns AccountAddress as a string in LONG form without a leading 0x.
+   */
+  toStringLongWithoutPrefix(): string {
+    return bytesToHex(this.data);
+  }
+
+  /**
+   * Get the inner hex data. The inner data is already a Uint8Array so no conversion
+   * is taking place here, it just returns the inner data.
+   *
+   * @returns Hex data as Uint8Array
+   */
+  toUint8Array(): Uint8Array {
+    return this.data;
+  }
+
+  // ===
+  // Methods for creating an instance of AccountAddress from other types.
+  // ===
+
+  /**
+   * NOTE: This function has strict parsing behavior. For relaxed behavior, please use
+   * the `fromStringRelaxed` function.
+   *
+   * Creates an instance of AccountAddress from a hex string.
+   *
+   * This function allows only the strictest formats defined by AIP-40. In short this
+   * means only the following formats are accepted:
+   *
+   * - LONG
+   * - SHORT for special addresses
+   *
+   * Where:
+   * - LONG is defined as 0x + 64 hex characters.
+   * - SHORT for special addresses is 0x0 to 0xf inclusive.
+   *
+   * This means the following are not accepted:
+   * - SHORT for non-special addresses.
+   * - Any address without a leading 0x.
+   *
+   * Learn more about the different address formats by reading AIP-40:
+   * https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
+   *
+   * @param args.input A hex string representing an account address.
+   *
+   * @returns An instance of AccountAddress.
+   */
+  static fromString(args: { input: string }): AccountAddress {
+    // Assert the string starts with 0x.
+    if (!args.input.startsWith("0x")) {
+      throw new ParsingError("Hex string must start with a leading 0x.", AddressInvalidReason.LEADING_ZERO_X_REQUIRED);
+    }
+
+    const address = AccountAddress.fromStringRelaxed(args);
+
+    // Assert that only special addresses can use short form.
+    if (args.input.slice(2).length !== this.LONG_STRING_LENGTH && !address.isSpecial()) {
+      throw new ParsingError(
+        "Hex string is not a special address, it must be represented as 0x + 64 chars.",
+        AddressInvalidReason.LONG_FORM_REQUIRED_UNLESS_SPECIAL,
+      );
+    }
+
+    return address;
+  }
+
+  /**
+   * NOTE: This function has relaxed parsing behavior. For strict behavior, please use
+   * the `fromString` function. Where possible use `fromString` rather than this
+   * function, `fromStringRelaxed` is only provided for backwards compatibility.
+   *
+   * Creates an instance of AccountAddress from a hex string.
+   *
+   * This function allows all formats defined by AIP-40. In short this means the
+   * following formats are accepted:
+   *
+   * - LONG, with or without leading 0x
+   * - SHORT, with or without leading 0x
+   *
+   * Where:
+   * - LONG is 64 hex characters.
+   * - SHORT is 1 to 63 hex characters inclusive.
+   *
+   * Learn more about the different address formats by reading AIP-40:
+   * https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
+   *
+   * @param args.input A hex string representing an account address.
+   *
+   * @returns An instance of AccountAddress.
+   */
+  static fromStringRelaxed(args: { input: string }): AccountAddress {
+    let { input } = args;
+
+    // Remove leading 0x for parsing.
+    if (input.startsWith("0x")) {
+      input = input.slice(2);
+    }
+
+    // Ensure the address string is at least 1 character long.
+    if (input.length === 0) {
+      throw new ParsingError(
+        "Hex string is too short, must be 1 to 64 chars long, excluding the leading 0x.",
+        AddressInvalidReason.TOO_SHORT,
+      );
+    }
+
+    // Ensure the address string is not longer than 64 characters.
+    if (input.length > 64) {
+      throw new ParsingError(
+        "Hex string is too long, must be 1 to 64 chars long, excluding the leading 0x.",
+        AddressInvalidReason.TOO_LONG,
+      );
+    }
+
+    let addressBytes: Uint8Array;
+    try {
+      // Pad the address with leading zeroes so it is 64 chars long and then convert
+      // the hex string to bytes. Every two characters in a hex string constitutes a
+      // single byte. So a 64 length hex string becomes a 32 byte array.
+      addressBytes = hexToBytes(input.padStart(64, "0"));
+    } catch (e) {
+      const error = e as Error;
+      // At this point the only way this can fail is if the hex string contains
+      // invalid characters.
+      throw new ParsingError(`Hex characters are invalid: ${error.message}`, AddressInvalidReason.INVALID_HEX_CHARS);
+    }
+
+    return new AccountAddress({ data: addressBytes });
+  }
+
+  /**
+   * Convenience method for creating an AccountAddress from HexInput. For more
+   * more information on how this works, see the constructor and fromString.
+   *
+   * @param args.input A hex string or Uint8Array representing an account address.
+   *
+   * @returns An instance of AccountAddress.
+   */
+  static fromHexInput(args: { input: HexInput }): AccountAddress {
+    if (args.input instanceof Uint8Array) {
+      return new AccountAddress({ data: args.input });
+    }
+    return AccountAddress.fromString({ input: args.input });
+  }
+
+  /**
+   * Convenience method for creating an AccountAddress from HexInput. For more
+   * more information on how this works, see the constructor and fromStringRelaxed.
+   *
+   * @param args.input A hex string or Uint8Array representing an account address.
+   *
+   * @returns An instance of AccountAddress.
+   */
+  static fromHexInputRelaxed(args: { input: HexInput }): AccountAddress {
+    if (args.input instanceof Uint8Array) {
+      return new AccountAddress({ data: args.input });
+    }
+    return AccountAddress.fromStringRelaxed({ input: args.input });
+  }
+
+  // ===
+  // Methods for checking validity.
+  // ===
+
+  /**
+   * Check if the string is a valid AccountAddress.
+   *
+   * @param str A hex string representing an account address.
+   * @param relaxed If true, use relaxed parsing behavior. If false, use strict parsing behavior.
+   *
+   * @returns valid = true if the string is valid, valid = false if not. If the string
+   * is not valid, invalidReason will be set explaining why it is invalid.
+   */
+  static isValid(args: { input: string; relaxed?: boolean }): ParsingResult<AddressInvalidReason> {
+    try {
+      if (args.relaxed) {
+        AccountAddress.fromStringRelaxed({ input: args.input });
+      } else {
+        AccountAddress.fromString({ input: args.input });
+      }
+      return { valid: true };
+    } catch (e) {
+      const error = e as ParsingError<AddressInvalidReason>;
+      return {
+        valid: false,
+        invalidReason: error.invalidReason,
+        invalidReasonMessage: error.message,
+      };
+    }
+  }
+}
diff --git a/ecosystem/typescript/sdk_v2/src/core/index.ts b/ecosystem/typescript/sdk_v2/src/core/index.ts
index 070dded028b7b1..4328f607f82dae 100644
--- a/ecosystem/typescript/sdk_v2/src/core/index.ts
+++ b/ecosystem/typescript/sdk_v2/src/core/index.ts
@@ -1,5 +1,6 @@
 // Copyright © Aptos Foundation
 // SPDX-License-Identifier: Apache-2.0
 
+export * from "./account_address";
 export * from "./common";
 export * from "./hex";
diff --git a/ecosystem/typescript/sdk_v2/tests/unit/account_address.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/account_address.test.ts
new file mode 100644
index 00000000000000..ea4537715852ad
--- /dev/null
+++ b/ecosystem/typescript/sdk_v2/tests/unit/account_address.test.ts
@@ -0,0 +1,329 @@
+// Copyright © Aptos Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+import { AccountAddress, AddressInvalidReason } from "../../src/core/account_address";
+
+type Addresses = {
+  shortWith0x: string;
+  shortWithout0x: string;
+  longWith0x: string;
+  longWithout0x: string;
+  bytes: Uint8Array;
+};
+
+// Special addresses.
+
+const ADDRESS_ZERO: Addresses = {
+  shortWith0x: "0x0",
+  shortWithout0x: "0",
+  longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000000",
+  longWithout0x: "0000000000000000000000000000000000000000000000000000000000000000",
+  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, 0, 0, 0, 0, 0, 0,
+  ]),
+};
+
+const ADDRESS_ONE: Addresses = {
+  shortWith0x: "0x1",
+  shortWithout0x: "1",
+  longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000001",
+  longWithout0x: "0000000000000000000000000000000000000000000000000000000000000001",
+  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, 0, 0, 0, 0, 0, 1,
+  ]),
+};
+
+const ADDRESS_F: Addresses = {
+  shortWith0x: "0xf",
+  shortWithout0x: "f",
+  longWith0x: "0x000000000000000000000000000000000000000000000000000000000000000f",
+  longWithout0x: "000000000000000000000000000000000000000000000000000000000000000f",
+  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, 0, 0, 0, 0, 0, 15,
+  ]),
+};
+
+// Non-special addresses.
+
+const ADDRESS_TEN: Addresses = {
+  shortWith0x: "0x10",
+  shortWithout0x: "10",
+  longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000010",
+  longWithout0x: "0000000000000000000000000000000000000000000000000000000000000010",
+  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, 0, 0, 0, 0, 0, 16,
+  ]),
+};
+
+const ADDRESS_OTHER: Addresses = {
+  shortWith0x: "0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0",
+  shortWithout0x: "ca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0",
+  // These are the same as the short variants.
+  longWith0x: "0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0",
+  longWithout0x: "ca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0",
+  bytes: new Uint8Array([
+    202, 132, 50, 121, 227, 66, 113, 68, 206, 173, 94, 77, 89, 153, 163, 208, 202, 132, 50, 121, 227, 66, 113, 68, 206,
+    173, 94, 77, 89, 153, 163, 208,
+  ]),
+};
+
+// These tests show that fromStringRelaxed works happily parses all formats.
+describe("AccountAddress fromStringRelaxed", () => {
+  it("parses special address: 0x0", async () => {
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.longWith0x }).toString()).toBe(
+      ADDRESS_ZERO.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.longWithout0x }).toString()).toBe(
+      ADDRESS_ZERO.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.shortWith0x }).toString()).toBe(
+      ADDRESS_ZERO.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ZERO.shortWithout0x }).toString()).toBe(
+      ADDRESS_ZERO.shortWith0x,
+    );
+  });
+
+  it("parses special address: 0x1", async () => {
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ONE.longWith0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ONE.longWithout0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ONE.shortWith0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_ONE.shortWithout0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+  });
+
+  it("parses special address: 0xf", async () => {
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_F.longWith0x }).toString()).toBe(ADDRESS_F.shortWith0x);
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_F.longWithout0x }).toString()).toBe(ADDRESS_F.shortWith0x);
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_F.shortWith0x }).toString()).toBe(ADDRESS_F.shortWith0x);
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_F.shortWithout0x }).toString()).toBe(
+      ADDRESS_F.shortWith0x,
+    );
+  });
+
+  it("parses non-special address: 0x10", async () => {
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_TEN.longWith0x }).toString()).toBe(ADDRESS_TEN.longWith0x);
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_TEN.longWithout0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_TEN.shortWith0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_TEN.shortWithout0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+  });
+
+  it("parses non-special address: 0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", async () => {
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_OTHER.longWith0x }).toString()).toBe(
+      ADDRESS_OTHER.longWith0x,
+    );
+    expect(AccountAddress.fromStringRelaxed({ input: ADDRESS_OTHER.longWithout0x }).toString()).toBe(
+      ADDRESS_OTHER.longWith0x,
+    );
+  });
+});
+
+// These tests show that fromString only parses addresses with a leading 0x and only
+// SHORT if it is a special address.
+describe("AccountAddress fromString", () => {
+  it("parses special address: 0x0", async () => {
+    expect(AccountAddress.fromString({ input: ADDRESS_ZERO.longWith0x }).toString()).toBe(ADDRESS_ZERO.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_ZERO.longWithout0x })).toThrow();
+    expect(AccountAddress.fromString({ input: ADDRESS_ZERO.shortWith0x }).toString()).toBe(ADDRESS_ZERO.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_ZERO.shortWithout0x })).toThrow();
+  });
+
+  it("parses special address: 0x1", async () => {
+    expect(AccountAddress.fromString({ input: ADDRESS_ONE.longWith0x }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_ONE.longWithout0x })).toThrow();
+    expect(AccountAddress.fromString({ input: ADDRESS_ONE.shortWith0x }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_ONE.shortWithout0x })).toThrow();
+  });
+
+  it("parses special address: 0xf", async () => {
+    expect(AccountAddress.fromString({ input: ADDRESS_F.longWith0x }).toString()).toBe(ADDRESS_F.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_F.longWithout0x })).toThrow();
+    expect(AccountAddress.fromString({ input: ADDRESS_F.shortWith0x }).toString()).toBe(ADDRESS_F.shortWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_F.shortWithout0x })).toThrow();
+  });
+
+  it("parses non-special address: 0x10", async () => {
+    expect(AccountAddress.fromString({ input: ADDRESS_TEN.longWith0x }).toString()).toBe(ADDRESS_TEN.longWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_TEN.longWithout0x })).toThrow();
+    expect(() => AccountAddress.fromString({ input: ADDRESS_TEN.shortWith0x })).toThrow();
+    expect(() => AccountAddress.fromString({ input: ADDRESS_TEN.shortWithout0x })).toThrow();
+  });
+
+  it("parses non-special address: 0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", async () => {
+    expect(AccountAddress.fromString({ input: ADDRESS_OTHER.longWith0x }).toString()).toBe(ADDRESS_OTHER.longWith0x);
+    expect(() => AccountAddress.fromString({ input: ADDRESS_OTHER.longWithout0x })).toThrow();
+  });
+});
+
+describe("AccountAddress fromHexInput", () => {
+  it("parses special address: 0x1", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_ONE.longWith0x }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_ONE.longWithout0x })).toThrow();
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_ONE.shortWith0x }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_ONE.shortWithout0x })).toThrow();
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_ONE.bytes }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+  });
+
+  it("parses non-special address: 0x10", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_TEN.longWith0x }).toString()).toBe(ADDRESS_TEN.longWith0x);
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_TEN.longWithout0x })).toThrow();
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_TEN.shortWith0x })).toThrow();
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_TEN.shortWithout0x })).toThrow();
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_TEN.bytes }).toString()).toBe(ADDRESS_TEN.longWith0x);
+  });
+
+  it("parses non-special address: 0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_OTHER.longWith0x }).toString()).toBe(ADDRESS_OTHER.longWith0x);
+    expect(() => AccountAddress.fromHexInput({ input: ADDRESS_OTHER.longWithout0x })).toThrow();
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_OTHER.bytes }).toString()).toBe(ADDRESS_OTHER.shortWith0x);
+  });
+});
+
+describe("AccountAddress fromHexInputRelaxed", () => {
+  it("parses special address: 0x1", async () => {
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_ONE.longWith0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_ONE.longWithout0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_ONE.shortWith0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_ONE.shortWithout0x }).toString()).toBe(
+      ADDRESS_ONE.shortWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_ONE.bytes }).toString()).toBe(ADDRESS_ONE.shortWith0x);
+  });
+
+  it("parses non-special address: 0x10", async () => {
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_TEN.longWith0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_TEN.longWithout0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_TEN.shortWith0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_TEN.shortWithout0x }).toString()).toBe(
+      ADDRESS_TEN.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_TEN.bytes }).toString()).toBe(ADDRESS_TEN.longWith0x);
+  });
+
+  it("parses non-special address: 0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", async () => {
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_OTHER.longWith0x }).toString()).toBe(
+      ADDRESS_OTHER.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_OTHER.longWithout0x }).toString()).toBe(
+      ADDRESS_OTHER.longWith0x,
+    );
+    expect(AccountAddress.fromHexInputRelaxed({ input: ADDRESS_OTHER.bytes }).toString()).toBe(
+      ADDRESS_OTHER.longWith0x,
+    );
+  });
+});
+
+describe("AccountAddress toUint8Array", () => {
+  it("correctly returns bytes for special address: 0x1", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_ONE.longWith0x }).toUint8Array()).toEqual(ADDRESS_ONE.bytes);
+  });
+
+  it("correctly returns bytes for  non-special address: 0x10", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_TEN.longWith0x }).toUint8Array()).toEqual(ADDRESS_TEN.bytes);
+  });
+
+  it("correctly returns bytes for  non-special address: 0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", async () => {
+    expect(AccountAddress.fromHexInput({ input: ADDRESS_OTHER.longWith0x }).toUint8Array()).toEqual(
+      ADDRESS_OTHER.bytes,
+    );
+  });
+});
+
+describe("AccountAddress toStringWithoutPrefix", () => {
+  it("formats special address correctly: 0x0", async () => {
+    const addr = AccountAddress.fromString({ input: ADDRESS_ZERO.shortWith0x });
+    expect(addr.toStringWithoutPrefix()).toBe(ADDRESS_ZERO.shortWithout0x);
+  });
+
+  it("formats non-special address correctly: 0x10", async () => {
+    const addr = AccountAddress.fromString({ input: ADDRESS_TEN.longWith0x });
+    expect(addr.toStringWithoutPrefix()).toBe(ADDRESS_TEN.longWithout0x);
+  });
+});
+
+describe("AccountAddress toStringLong", () => {
+  it("formats special address correctly: 0x0", async () => {
+    const addr = AccountAddress.fromString({ input: ADDRESS_ZERO.shortWith0x });
+    expect(addr.toStringLong()).toBe(ADDRESS_ZERO.longWith0x);
+  });
+
+  it("formats non-special address correctly: 0x10", async () => {
+    const addr = AccountAddress.fromString({ input: ADDRESS_TEN.longWith0x });
+    expect(addr.toStringLong()).toBe(ADDRESS_TEN.longWith0x);
+  });
+});
+
+describe("AccountAddress toStringLongWithoutPrefix", () => {
+  it("formats special address correctly: 0x0", async () => {
+    console.log(AccountAddress.fromHexInput({ input: ADDRESS_ZERO.shortWith0x }).toUint8Array());
+    const addr = AccountAddress.fromString({ input: ADDRESS_ZERO.shortWith0x });
+    expect(addr.toStringLongWithoutPrefix()).toBe(ADDRESS_ZERO.longWithout0x);
+  });
+
+  it("formats non-special address correctly: 0x10", async () => {
+    const addr = AccountAddress.fromString({ input: ADDRESS_TEN.longWith0x });
+    expect(addr.toStringLongWithoutPrefix()).toBe(ADDRESS_TEN.longWithout0x);
+  });
+});
+
+describe("AccountAddress other parsing", () => {
+  it("throws exception when initiating from too long hex string", async () => {
+    expect(() => {
+      AccountAddress.fromString({ input: `${ADDRESS_ONE.longWith0x}1` });
+    }).toThrow("Hex string is too long, must be 1 to 64 chars long, excluding the leading 0x.");
+  });
+
+  test("throws when parsing invalid hex char", () => {
+    expect(() => AccountAddress.fromString({ input: "0xxyz" })).toThrow();
+  });
+
+  test("throws when parsing account address of length zero", () => {
+    expect(() => AccountAddress.fromString({ input: "0x" })).toThrow();
+    expect(() => AccountAddress.fromString({ input: "" })).toThrow();
+  });
+
+  test("throws when parsing invalid prefix", () => {
+    expect(() => AccountAddress.fromString({ input: "0za" })).toThrow();
+  });
+
+  it("isValid is false if too long with 0xf", async () => {
+    const { valid, invalidReason, invalidReasonMessage } = AccountAddress.isValid({
+      input: `0x00${ADDRESS_F.longWithout0x}`,
+    });
+    expect(valid).toBe(false);
+    expect(invalidReason).toBe(AddressInvalidReason.TOO_LONG);
+    expect(invalidReasonMessage).toBe("Hex string is too long, must be 1 to 64 chars long, excluding the leading 0x.");
+  });
+
+  it("isValid is true if account address string is valid", async () => {
+    const { valid, invalidReason, invalidReasonMessage } = AccountAddress.isValid({ input: ADDRESS_F.longWith0x });
+    expect(valid).toBe(true);
+    expect(invalidReason).toBeUndefined();
+    expect(invalidReasonMessage).toBeUndefined();
+  });
+});