-
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.
[TS SDK v2] Add Hex and HexInput types (#9595)
Co-authored-by: maayan <[email protected]>
- Loading branch information
Showing
6 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
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
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,40 @@ | ||
// Copyright © Aptos Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/** | ||
* This error is used to explain why parsing failed. | ||
*/ | ||
export class ParsingError<T> extends Error { | ||
/** | ||
* This provides a programmatic way to access why parsing failed. 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. This should be an enum. | ||
*/ | ||
public invalidReason: T; | ||
|
||
constructor(message: string, invalidReason: T) { | ||
super(message); | ||
this.invalidReason = invalidReason; | ||
} | ||
} | ||
|
||
/** | ||
* Whereas ParsingError is thrown when parsing fails, e.g. in a fromString function, | ||
* this type is returned from "defensive" functions like isValid. | ||
*/ | ||
export type ParsingResult<T> = { | ||
/** | ||
* True if valid, false otherwise. | ||
*/ | ||
valid: boolean; | ||
|
||
/* | ||
* If valid is false, this will be a code explaining why parsing failed. | ||
*/ | ||
invalidReason?: T; | ||
|
||
/* | ||
* If valid is false, this will be a string explaining why parsing failed. | ||
*/ | ||
invalidReasonMessage?: string; | ||
}; |
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,165 @@ | ||
// 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 parsing might have failed. | ||
*/ | ||
export enum HexInvalidReason { | ||
TOO_SHORT = "too_short", | ||
INVALID_LENGTH = "invalid_length", | ||
INVALID_HEX_CHARS = "invalid_hex_chars", | ||
} | ||
|
||
/** | ||
* NOTE: Do not use this class when working with account addresses, use AccountAddress. | ||
* | ||
* NOTE: When accepting hex data as input to a function, prefer to accept HexInput and | ||
* then use the static helper methods of this class to convert it into the desired | ||
* format. This enables the greatest flexibility for the developer. | ||
* | ||
* Hex is a helper class for working with hex data. Hex data, when represented as a | ||
* string, generally looks like this, for example: 0xaabbcc, 45cd32, etc. | ||
* | ||
* You might use this class like this: | ||
* | ||
* ```ts | ||
* getTransactionByHash(txnHash: HexInput): Promise<Transaction> { | ||
* const txnHashString = Hex.fromHexInput({ hexInput: txnHash }).toString(); | ||
* return await getTransactionByHashInner(txnHashString); | ||
* } | ||
* ``` | ||
* | ||
* This call to `Hex.fromHexInput().toString()` converts the HexInput to a hex string | ||
* with a leading 0x prefix, regardless of what the input format was. | ||
* | ||
* These are some other ways to chain the functions together: | ||
* - `Hex.fromString({ hexInput: "0x1f" }).toUint8Array()` | ||
* - `new Hex({ data: [1, 3] }).toStringWithoutPrefix()` | ||
*/ | ||
export class Hex { | ||
private data: Uint8Array; | ||
|
||
/** | ||
* Create a new Hex instance from a Uint8Array. | ||
* | ||
* @param hex Uint8Array | ||
*/ | ||
constructor(args: { data: Uint8Array }) { | ||
this.data = args.data; | ||
} | ||
|
||
// === | ||
// Methods for representing an instance of Hex as other types. | ||
// === | ||
|
||
/** | ||
* 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; | ||
} | ||
|
||
/** | ||
* Get the hex data as a string without the 0x prefix. | ||
* | ||
* @returns Hex string without 0x prefix | ||
*/ | ||
toStringWithoutPrefix(): string { | ||
return bytesToHex(this.data); | ||
} | ||
|
||
/** | ||
* Get the hex data as a string with the 0x prefix. | ||
* | ||
* @returns Hex string with 0x prefix | ||
*/ | ||
toString(): string { | ||
return `0x${this.toStringWithoutPrefix()}`; | ||
} | ||
|
||
// === | ||
// Methods for creating an instance of Hex from other types. | ||
// === | ||
|
||
/** | ||
* Static method to convert a hex string to Hex | ||
* | ||
* @param str A hex string, with or without the 0x prefix | ||
* | ||
* @returns Hex | ||
*/ | ||
static fromString(args: { str: string }): Hex { | ||
let input = args.str; | ||
|
||
if (input.startsWith("0x")) { | ||
input = input.slice(2); | ||
} | ||
|
||
if (input.length === 0) { | ||
throw new ParsingError( | ||
"Hex string is too short, must be at least 1 char long, excluding the optional leading 0x.", | ||
HexInvalidReason.TOO_SHORT, | ||
); | ||
} | ||
|
||
if (input.length % 2 !== 0) { | ||
throw new ParsingError("Hex string must be an even number of hex characters.", HexInvalidReason.INVALID_LENGTH); | ||
} | ||
|
||
try { | ||
return new Hex({ data: hexToBytes(input) }); | ||
} catch (e) { | ||
const error = e as Error; | ||
throw new ParsingError( | ||
`Hex string contains invalid hex characters: ${error.message}`, | ||
HexInvalidReason.INVALID_HEX_CHARS, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Static method to convert an instance of HexInput to Hex | ||
* | ||
* @param str A HexInput (string or Uint8Array) | ||
* | ||
* @returns Hex | ||
*/ | ||
static fromHexInput(args: { hexInput: HexInput }): Hex { | ||
if (args.hexInput instanceof Uint8Array) return new Hex({ data: args.hexInput }); | ||
return Hex.fromString({ str: args.hexInput }); | ||
} | ||
|
||
// === | ||
// Methods for checking validity. | ||
// === | ||
|
||
/** | ||
* Check if the string is valid hex. | ||
* | ||
* @param str A hex string representing byte data. | ||
* | ||
* @returns valid = true if the string is valid, false if not. If the string is not | ||
* valid, invalidReason and invalidReasonMessage will be set explaining why it is | ||
* invalid. | ||
*/ | ||
static isValid(args: { str: string }): ParsingResult<HexInvalidReason> { | ||
try { | ||
Hex.fromString(args); | ||
return { valid: true }; | ||
} catch (e) { | ||
const error = e as ParsingError<HexInvalidReason>; | ||
return { | ||
valid: false, | ||
invalidReason: error.invalidReason, | ||
invalidReasonMessage: error.message, | ||
}; | ||
} | ||
} | ||
} |
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,5 @@ | ||
// Copyright © Aptos Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export * from "./common"; | ||
export * from "./hex"; |
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 +1,2 @@ | ||
export type AnyNumber = number | bigint; | ||
export type HexInput = string | Uint8Array; |
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,92 @@ | ||
import { ParsingError } from "../../src/core"; | ||
import { Hex, HexInvalidReason } from "../../src/core/hex"; | ||
|
||
const mockHex = { | ||
withoutPrefix: "007711b4d0", | ||
withPrefix: "0x007711b4d0", | ||
bytes: new Uint8Array([0, 119, 17, 180, 208]), | ||
}; | ||
|
||
test("creates a new Hex instance from bytes", () => { | ||
const hex = new Hex({ data: mockHex.bytes }); | ||
expect(hex.toUint8Array()).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("creates a new Hex instance from string", () => { | ||
const hex = new Hex({ data: mockHex.bytes }); | ||
expect(hex.toString()).toEqual(mockHex.withPrefix); | ||
}); | ||
|
||
test("converts hex bytes input into hex data", () => { | ||
const hex = new Hex({ data: mockHex.bytes }); | ||
expect(hex instanceof Hex).toBeTruthy(); | ||
expect(hex.toUint8Array()).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("converts hex string input into hex data", () => { | ||
const hex = Hex.fromString({ str: mockHex.withPrefix }); | ||
expect(hex instanceof Hex).toBeTruthy(); | ||
expect(hex.toUint8Array()).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("accepts hex string input without prefix", () => { | ||
const hex = Hex.fromString({ str: mockHex.withoutPrefix }); | ||
expect(hex instanceof Hex).toBeTruthy(); | ||
expect(hex.toUint8Array()).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("accepts hex string with prefix", () => { | ||
const hex = Hex.fromString({ str: mockHex.withPrefix }); | ||
expect(hex instanceof Hex).toBeTruthy(); | ||
expect(hex.toUint8Array()).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("converts hex string to bytes", () => { | ||
const hex = Hex.fromHexInput({ hexInput: mockHex.withPrefix }).toUint8Array(); | ||
expect(hex instanceof Uint8Array).toBeTruthy(); | ||
expect(hex).toEqual(mockHex.bytes); | ||
}); | ||
|
||
test("converts hex bytes to string", () => { | ||
const hex = Hex.fromHexInput({ hexInput: mockHex.bytes }).toString(); | ||
expect(typeof hex).toEqual("string"); | ||
expect(hex).toEqual(mockHex.withPrefix); | ||
}); | ||
|
||
test("converts hex bytes to string without 0x prefix", () => { | ||
const hex = Hex.fromHexInput({ hexInput: mockHex.withPrefix }).toStringWithoutPrefix(); | ||
expect(hex).toEqual(mockHex.withoutPrefix); | ||
}); | ||
|
||
test("throws when parsing invalid hex char", () => { | ||
expect(() => Hex.fromString({ str: "0xzyzz" })).toThrow( | ||
"Hex string contains invalid hex characters: Invalid byte sequence", | ||
); | ||
}); | ||
|
||
test("throws when parsing hex of length zero", () => { | ||
expect(() => Hex.fromString({ str: "0x" })).toThrow( | ||
"Hex string is too short, must be at least 1 char long, excluding the optional leading 0x.", | ||
); | ||
expect(() => Hex.fromString({ str: "" })).toThrow( | ||
"Hex string is too short, must be at least 1 char long, excluding the optional leading 0x.", | ||
); | ||
}); | ||
|
||
test("throws when parsing hex of invalid length", () => { | ||
expect(() => Hex.fromString({ str: "0x1" })).toThrow("Hex string must be an even number of hex characters."); | ||
}); | ||
|
||
test("isValid returns true when parsing valid string", () => { | ||
const result = Hex.isValid({ str: "0x11aabb" }); | ||
expect(result.valid).toBe(true); | ||
expect(result.invalidReason).toBeUndefined(); | ||
expect(result.invalidReasonMessage).toBeUndefined(); | ||
}); | ||
|
||
test("isValid returns false when parsing hex of invalid length", () => { | ||
const result = Hex.isValid({ str: "0xa" }); | ||
expect(result.valid).toBe(false); | ||
expect(result.invalidReason).toBe(HexInvalidReason.INVALID_LENGTH); | ||
expect(result.invalidReasonMessage).toBe("Hex string must be an even number of hex characters."); | ||
}); |