-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
273 changes: 273 additions & 0 deletions
273
ecosystem/typescript/sdk_v2/src/types/account_address.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
// Copyright © Aptos Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; | ||
import { Serializer, Deserializer, Bytes } from "../bcs"; | ||
|
||
/** | ||
* 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 a helper class for working with account addresses. Account addresses, | ||
* when represented as a string, generally look like this for example: | ||
* - 0x1 | ||
* - 0xaa86fe99004361f747f91342ca13c426ca0cccb0c1217677180c9493bad6ef0c | ||
* | ||
* todo | ||
*/ | ||
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 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. | ||
* | ||
* 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 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); | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
isSpecial(): boolean { | ||
return ( | ||
this.address.slice(0, this.address.length - 1).every((byte) => byte === 0) && | ||
this.address[this.address.length - 1] < 0b10000 | ||
); | ||
} | ||
|
||
/** | ||
* Conforms to AIP 40. TODO: Write more. | ||
*/ | ||
toString(): string { | ||
let hex = bytesToHex(this.address); | ||
if (this.isSpecial()) { | ||
hex = hex[hex.length - 1]; | ||
} | ||
return `0x${this.toStringWithoutPrefix()}`; | ||
} | ||
|
||
/** | ||
* todo | ||
*/ | ||
toStringWithoutPrefix(): string { | ||
let hex = bytesToHex(this.address); | ||
if (this.isSpecial()) { | ||
hex = hex[hex.length - 1]; | ||
} | ||
return hex; | ||
} | ||
|
||
/* | ||
* Whereas toString will format special addresses (as defined by isSpecial) using the | ||
* short form (no leading 0s), this function will include leading zeroes. This is | ||
* allowed as per AIP 40 if the need arises, but using toString is preferred. | ||
*/ | ||
toStringLong(): string { | ||
return `0x${this.toStringLongWithoutPrefix()}`; | ||
} | ||
|
||
/* | ||
* todo | ||
*/ | ||
toStringLongWithoutPrefix(): string { | ||
return bytesToHex(this.address); | ||
} | ||
|
||
// For use with the BCS payload generation library. | ||
serialize(serializer: Serializer): void { | ||
serializer.serializeFixedBytes(this.address); | ||
} | ||
|
||
// For use with the BCS payload generation library. | ||
static deserialize(deserializer: Deserializer): AccountAddress { | ||
return new AccountAddress(deserializer.deserializeFixedBytes(AccountAddress.LENGTH)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
Oops, something went wrong.