From 5d27f322a5784cf5bddfab1e65bd5bf9a8ee7eaf Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:19:11 +0300 Subject: [PATCH 1/3] feat(core): `hexFrom` passthru normalized hex and `numToHex` enforce hex normalization --- .changeset/shiny-ants-say.md | 5 +++++ packages/core/src/hex/index.ts | 40 ++++++++++++++++++++++++++++++++-- packages/core/src/num/index.ts | 18 ++++++++++++--- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 .changeset/shiny-ants-say.md diff --git a/.changeset/shiny-ants-say.md b/.changeset/shiny-ants-say.md new file mode 100644 index 000000000..51df28401 --- /dev/null +++ b/.changeset/shiny-ants-say.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": patch +--- + +`hexFrom` passthru normalized hex and `numToHex` enforce hex normalization \ No newline at end of file diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 41df43d76..5712cc133 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -13,8 +13,39 @@ export type Hex = `0x${string}`; export type HexLike = BytesLike; /** - * Converts a HexLike value to a Hex string. - * @public + * Determines whether a given string is a properly formatted hexadecimal string (ccc.Hex). + * + * A valid hexadecimal string: + * - Has at least two characters. + * - Starts with "0x". + * - Has an even length. + * - Contains only characters representing digits (0-9) or lowercase letters (a-f) after the "0x" prefix. + * + * @param s - The string to validate as a hexadecimal (ccc.Hex) string. + * @returns True if the string is a valid hex string, false otherwise. + */ +export function isHex(s: string): s is Hex { + if ( + s.length < 2 || + s.charCodeAt(0) !== 48 || // ascii code for '0' + s.charCodeAt(1) !== 120 || // ascii code for 'x' + s.length % 2 !== 0 + ) { + return false; + } + + for (let i = 2; i < s.length; i++) { + const c = s.charCodeAt(i); + // Allow characters '0'-'9' and 'a'-'f' + if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) { + return false; + } + } + return true; +} + +/** + * Returns the hexadecimal representation of the given value. * * @param hex - The value to convert, which can be a string, Uint8Array, ArrayBuffer, or number array. * @returns A Hex string representing the value. @@ -26,5 +57,10 @@ export type HexLike = BytesLike; * ``` */ export function hexFrom(hex: HexLike): Hex { + // Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields. + if (typeof hex === "string" && isHex(hex)) { + return hex; + } + return `0x${bytesTo(bytesFrom(hex), "hex")}`; } diff --git a/packages/core/src/num/index.ts b/packages/core/src/num/index.ts index 8e1905a52..cf2367447 100644 --- a/packages/core/src/num/index.ts +++ b/packages/core/src/num/index.ts @@ -1,4 +1,5 @@ import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../bytes/index.js"; +import { Zero } from "../fixedPoint/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; /** @@ -90,11 +91,16 @@ export function numFrom(val: NumLike): Num { } /** - * Converts a NumLike value to a hexadecimal string. + * Convert a NumLike value into a canonical Hex, so prefixed with `0x` and + * containing an even number of lowercase hex digits (full-byte representation). + * * @public * * @param val - The value to convert, which can be a string, number, bigint, or HexLike. - * @returns A Hex string representing the numeric value. + * @returns A Hex string representing the provided value, prefixed with `0x` and + * containing an even number of lowercase hex digits. + * + * @throws {Error} If the normalized numeric value is negative. * * @example * ```typescript @@ -102,7 +108,13 @@ export function numFrom(val: NumLike): Num { * ``` */ export function numToHex(val: NumLike): Hex { - return `0x${numFrom(val).toString(16)}`; + const v = numFrom(val); + if (v < Zero) { + throw new Error("value must be non-negative"); + } + const h = v.toString(16); + // ensure even length (full bytes) + return h.length % 2 === 0 ? `0x${h}` : `0x0${h}`; } /** From b52feda1fe23b123cbaf3d8834d31b24b19ea7fd Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:22:49 +0300 Subject: [PATCH 2/3] feat(core): improve `isHex` --- packages/core/src/hex/index.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 5712cc133..cf8e7ab73 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -13,7 +13,7 @@ export type Hex = `0x${string}`; export type HexLike = BytesLike; /** - * Determines whether a given string is a properly formatted hexadecimal string (ccc.Hex). + * Determines whether a given value is a properly formatted hexadecimal string (ccc.Hex). * * A valid hexadecimal string: * - Has at least two characters. @@ -24,20 +24,14 @@ export type HexLike = BytesLike; * @param s - The string to validate as a hexadecimal (ccc.Hex) string. * @returns True if the string is a valid hex string, false otherwise. */ -export function isHex(s: string): s is Hex { - if ( - s.length < 2 || - s.charCodeAt(0) !== 48 || // ascii code for '0' - s.charCodeAt(1) !== 120 || // ascii code for 'x' - s.length % 2 !== 0 - ) { +export function isHex(s: unknown): s is Hex { + if (!(typeof s === "string" && s.length % 2 === 0 && s.startsWith("0x"))) { return false; } for (let i = 2; i < s.length; i++) { - const c = s.charCodeAt(i); - // Allow characters '0'-'9' and 'a'-'f' - if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) { + const c = s.charAt(i); + if (!(("0" <= c && c <= "9") || ("a" <= c && c <= "f"))) { return false; } } @@ -58,7 +52,7 @@ export function isHex(s: string): s is Hex { */ export function hexFrom(hex: HexLike): Hex { // Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields. - if (typeof hex === "string" && isHex(hex)) { + if (isHex(hex)) { return hex; } From 565c4bbd23ea50d5ed4a1d5fbeab58eac94e8827 Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:38:39 +0300 Subject: [PATCH 3/3] feat(core): improve `isHex` parameter --- packages/core/src/hex/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index cf8e7ab73..88a32d489 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -21,16 +21,16 @@ export type HexLike = BytesLike; * - Has an even length. * - Contains only characters representing digits (0-9) or lowercase letters (a-f) after the "0x" prefix. * - * @param s - The string to validate as a hexadecimal (ccc.Hex) string. + * @param v - The value to validate as a hexadecimal (ccc.Hex) string. * @returns True if the string is a valid hex string, false otherwise. */ -export function isHex(s: unknown): s is Hex { - if (!(typeof s === "string" && s.length % 2 === 0 && s.startsWith("0x"))) { +export function isHex(v: unknown): v is Hex { + if (!(typeof v === "string" && v.length % 2 === 0 && v.startsWith("0x"))) { return false; } - for (let i = 2; i < s.length; i++) { - const c = s.charAt(i); + for (let i = 2; i < v.length; i++) { + const c = v.charAt(i); if (!(("0" <= c && c <= "9") || ("a" <= c && c <= "f"))) { return false; }