Skip to content

Commit

Permalink
[TS SDK v2] Add AccountAddress
Browse files Browse the repository at this point in the history
  • Loading branch information
banool committed Aug 10, 2023
1 parent bfd588a commit 15268f1
Show file tree
Hide file tree
Showing 3 changed files with 524 additions and 0 deletions.
317 changes: 317 additions & 0 deletions ecosystem/typescript/sdk_v2/src/types/account_address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { Serializer, Deserializer, Bytes } from "../bcs";
import { HexInput } from ".";

/**
* 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",
ADDRESS_TOO_SHORT = "address_too_short",
ADDRESS_TOO_LONG = "address_too_long",
LEADING_ZERO_X_REQUIRED = "leading_zero_x_required",
LONG_FORM_REQUIRED_UNLESS_SPECIAL = "long_form_required_unless_special",
}

/**
* This error is used to explain why an address was invalid.
*/
export class AddressInvalidError extends Error {
// This provides a programmatic way to access why an address was invalid. Downstream
// devs might want to use this to build their own error messages if the default error
// messages are not suitable for their use case.
public reason: AddressInvalidReason;

constructor(message: string, reason: AddressInvalidReason) {
super(message);
this.reason = reason;
}
}

/**
* 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 this standard, read the AIP here:
* https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md.
*/
export class AccountAddress {
// 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;

// This is the internal representation of an account address.
readonly address: Bytes;

static ADDRESS_ONE: AccountAddress = AccountAddress.fromString({ str: "0x1" });

static ADDRESS_TWO: AccountAddress = AccountAddress.fromString({ str: "0x2" });

static ADDRESS_THREE: AccountAddress = AccountAddress.fromString({ str: "0x3" });

static ADDRESS_FOUR: AccountAddress = AccountAddress.fromString({ str: "0x4" });

constructor(address: Bytes) {
if (address.length !== AccountAddress.LENGTH) {
throw new AddressInvalidError("Expected address of length 32", AddressInvalidReason.INCORRECT_NUMBER_OF_BYTES);
}
this.address = address;
}

/**
* NOTE: This function has strict parsing behavior. For relaxed behavior, please use
* the `fromStringRelaxed` function.
*
* Creates 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
*
* 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.str A hex string representing an account address.
*/
static fromString(args: { str: string }): AccountAddress {
let input = args.str;

// Assert the string starts with 0x.
if (!input.startsWith("0x")) {
throw new AddressInvalidError(
"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 (input.slice(2).length !== this.LONG_STRING_LENGTH && !address.isSpecial()) {
throw new AddressInvalidError(
"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.
*
* Creates 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
*
* LONG is 64 hex characters.
*
* SHORT is 1 to 63 hex characters.
*
* 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.str A hex string representing an account address.
*/
static fromStringRelaxed(args: { str: string }): AccountAddress {
let input = args.str;

// 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 AddressInvalidError(
"Hex string is too short, must be 1 to 64 chars long, excluding the leading 0x.",
AddressInvalidReason.ADDRESS_TOO_SHORT,
);
}

// Ensure the address string is not longer than 64 characters.
if (input.length > 64) {
throw new AddressInvalidError(
"Hex string is too long, must be 1 to 64 chars long, excluding the leading 0x.",
AddressInvalidReason.ADDRESS_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 AddressInvalidError(
`Hex characters are invalid: ${error.message}`,
AddressInvalidReason.INVALID_HEX_CHARS,
);
}

return new AccountAddress(addressBytes);
}

/**
* Convenience method for creating an AccountAddress from HexInput. For more
* more information on how this works, see the constructor and fromString.
*/
fromHexInput(args: { hexInput: HexInput }): AccountAddress {
if (args.hexInput instanceof Uint8Array) {
return new AccountAddress(args.hexInput);
}
return AccountAddress.fromString({ str: args.hexInput });
}

/**
* Convenience method for creating an AccountAddress from HexInput. For more
* more information on how this works, see the constructor and fromStringRelaxed.
*/
fromHexInputRelaxed(args: { hexInput: HexInput }): AccountAddress {
if (args.hexInput instanceof Uint8Array) {
return new AccountAddress(args.hexInput);
}
return AccountAddress.fromStringRelaxed({ str: args.hexInput });
}

/**
* 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 valud = 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: { str: string; relaxed?: boolean }): {
valid: boolean;
invalidReason: string | null;
invalidReasonCode: AddressInvalidReason | null;
} {
try {
if (args.relaxed) {
AccountAddress.fromStringRelaxed({ str: args.str });
} else {
AccountAddress.fromString({ str: args.str });
}
return { valid: true, invalidReason: null, invalidReasonCode: null };
} catch (e) {
const addressInvalidError = e as AddressInvalidError;
return {
valid: false,
invalidReason: addressInvalidError.message,
invalidReasonCode: addressInvalidError.reason,
};
}
}

/**
* 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.
*
* @returns true if the address is special, false if not.
*/
isSpecial(): boolean {
return (
this.address.slice(0, this.address.length - 1).every((byte) => byte === 0) &&
this.address[this.address.length - 1] < 0b10000
);
}

/**
* 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 {
let hex = bytesToHex(this.address);
if (this.isSpecial()) {
hex = hex[hex.length - 1];
}
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`.
*/
toStringWithoutPrefix(): string {
let hex = bytesToHex(this.address);
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 function will include leading zeroes.
*/
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.
*/
toStringLongWithoutPrefix(): string {
return bytesToHex(this.address);
}

// For use with the BCS payload generation library.
serialize(args: { serializer: Serializer }): void {
args.serializer.serializeFixedBytes(this.address);
}

// For use with the BCS payload generation library.
static deserialize(args: { deserializer: Deserializer }): AccountAddress {
return new AccountAddress(args.deserializer.deserializeFixedBytes(AccountAddress.LENGTH));
}
}
1 change: 1 addition & 0 deletions ecosystem/typescript/sdk_v2/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type AnyNumber = number | bigint;
export type HexInput = string | Uint8Array;

export * from "./account_address";
export * from "./hex";
Loading

0 comments on commit 15268f1

Please sign in to comment.