Skip to content

Commit 5d27f32

Browse files
committed
feat(core): hexFrom passthru normalized hex and numToHex enforce hex normalization
1 parent e2ce0a2 commit 5d27f32

File tree

3 files changed

+58
-5
lines changed

3 files changed

+58
-5
lines changed

.changeset/shiny-ants-say.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ckb-ccc/core": patch
3+
---
4+
5+
`hexFrom` passthru normalized hex and `numToHex` enforce hex normalization

packages/core/src/hex/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,39 @@ export type Hex = `0x${string}`;
1313
export type HexLike = BytesLike;
1414

1515
/**
16-
* Converts a HexLike value to a Hex string.
17-
* @public
16+
* Determines whether a given string is a properly formatted hexadecimal string (ccc.Hex).
17+
*
18+
* A valid hexadecimal string:
19+
* - Has at least two characters.
20+
* - Starts with "0x".
21+
* - Has an even length.
22+
* - Contains only characters representing digits (0-9) or lowercase letters (a-f) after the "0x" prefix.
23+
*
24+
* @param s - The string to validate as a hexadecimal (ccc.Hex) string.
25+
* @returns True if the string is a valid hex string, false otherwise.
26+
*/
27+
export function isHex(s: string): s is Hex {
28+
if (
29+
s.length < 2 ||
30+
s.charCodeAt(0) !== 48 || // ascii code for '0'
31+
s.charCodeAt(1) !== 120 || // ascii code for 'x'
32+
s.length % 2 !== 0
33+
) {
34+
return false;
35+
}
36+
37+
for (let i = 2; i < s.length; i++) {
38+
const c = s.charCodeAt(i);
39+
// Allow characters '0'-'9' and 'a'-'f'
40+
if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) {
41+
return false;
42+
}
43+
}
44+
return true;
45+
}
46+
47+
/**
48+
* Returns the hexadecimal representation of the given value.
1849
*
1950
* @param hex - The value to convert, which can be a string, Uint8Array, ArrayBuffer, or number array.
2051
* @returns A Hex string representing the value.
@@ -26,5 +57,10 @@ export type HexLike = BytesLike;
2657
* ```
2758
*/
2859
export function hexFrom(hex: HexLike): Hex {
60+
// Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields.
61+
if (typeof hex === "string" && isHex(hex)) {
62+
return hex;
63+
}
64+
2965
return `0x${bytesTo(bytesFrom(hex), "hex")}`;
3066
}

packages/core/src/num/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../bytes/index.js";
2+
import { Zero } from "../fixedPoint/index.js";
23
import { Hex, HexLike, hexFrom } from "../hex/index.js";
34

45
/**
@@ -90,19 +91,30 @@ export function numFrom(val: NumLike): Num {
9091
}
9192

9293
/**
93-
* Converts a NumLike value to a hexadecimal string.
94+
* Convert a NumLike value into a canonical Hex, so prefixed with `0x` and
95+
* containing an even number of lowercase hex digits (full-byte representation).
96+
*
9497
* @public
9598
*
9699
* @param val - The value to convert, which can be a string, number, bigint, or HexLike.
97-
* @returns A Hex string representing the numeric value.
100+
* @returns A Hex string representing the provided value, prefixed with `0x` and
101+
* containing an even number of lowercase hex digits.
102+
*
103+
* @throws {Error} If the normalized numeric value is negative.
98104
*
99105
* @example
100106
* ```typescript
101107
* const hex = numToHex(12345); // Outputs "0x3039"
102108
* ```
103109
*/
104110
export function numToHex(val: NumLike): Hex {
105-
return `0x${numFrom(val).toString(16)}`;
111+
const v = numFrom(val);
112+
if (v < Zero) {
113+
throw new Error("value must be non-negative");
114+
}
115+
const h = v.toString(16);
116+
// ensure even length (full bytes)
117+
return h.length % 2 === 0 ? `0x${h}` : `0x0${h}`;
106118
}
107119

108120
/**

0 commit comments

Comments
 (0)