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..88a32d489 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -13,8 +13,33 @@ export type Hex = `0x${string}`; export type HexLike = BytesLike; /** - * Converts a HexLike value to a Hex string. - * @public + * Determines whether a given value 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 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(v: unknown): v is Hex { + if (!(typeof v === "string" && v.length % 2 === 0 && v.startsWith("0x"))) { + return false; + } + + for (let i = 2; i < v.length; i++) { + const c = v.charAt(i); + if (!(("0" <= c && c <= "9") || ("a" <= c && c <= "f"))) { + 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 +51,10 @@ export type HexLike = BytesLike; * ``` */ export function hexFrom(hex: HexLike): Hex { + // Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields. + if (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}`; } /**