From 2d74e3997e112b097db27200e5ee34ed23aff4ca Mon Sep 17 00:00:00 2001 From: Erhnysr Date: Sun, 16 Aug 2026 13:42:08 +0300 Subject: [PATCH 1/3] fix(keys): accept a bare 0x prefix in isHexString --- .changeset/is-hex-string-empty-body.md | 5 +++++ packages/keys/src/encoding/hex.test.ts | 8 ++++++++ packages/keys/src/encoding/hex.ts | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/is-hex-string-empty-body.md diff --git a/.changeset/is-hex-string-empty-body.md b/.changeset/is-hex-string-empty-body.md new file mode 100644 index 00000000..ccde0b9a --- /dev/null +++ b/.changeset/is-hex-string-empty-body.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/keys": patch +--- + +`isHexString` now returns `true` for a bare `0x` prefix with an empty body, matching its documented behavior. The check required at least one hex digit after the prefix, so `isHexString("0x")` returned `false` even though the JSDoc example states it returns `true`. A bare empty string with no prefix (`""`) continues to return `false`. diff --git a/packages/keys/src/encoding/hex.test.ts b/packages/keys/src/encoding/hex.test.ts index c66e3b36..9977fa97 100644 --- a/packages/keys/src/encoding/hex.test.ts +++ b/packages/keys/src/encoding/hex.test.ts @@ -44,6 +44,14 @@ describe("isHexString", () => { expect(isHexString("1234567890abcdef")).toBe(true) }) + test("returns true for a bare 0x prefix with an empty body", () => { + expect(isHexString("0x")).toBe(true) + }) + + test("returns false for an empty string with no prefix", () => { + expect(isHexString("")).toBe(false) + }) + test("returns false for invalid hex strings", () => { expect(isHexString("0x1234567890abcdefg")).toBe(false) expect(isHexString("not hex")).toBe(false) diff --git a/packages/keys/src/encoding/hex.ts b/packages/keys/src/encoding/hex.ts index aa4de17b..4a084790 100644 --- a/packages/keys/src/encoding/hex.ts +++ b/packages/keys/src/encoding/hex.ts @@ -47,6 +47,14 @@ export function isHexString(value: unknown): value is string { return false } - const hexWithoutPrefix = value.startsWith("0x") ? value.slice(2) : value + const hasPrefix = value.startsWith("0x") + const hexWithoutPrefix = hasPrefix ? value.slice(2) : value + + // A bare "0x" prefix has an empty body and is a valid (zero-length) hex + // string, as documented above. An empty string with no prefix is not. + if (hexWithoutPrefix.length === 0) { + return hasPrefix + } + return /^[0-9A-Fa-f]+$/.test(hexWithoutPrefix) } From 124591e5c99e71c21f51b23529d8dacd086f7d8d Mon Sep 17 00:00:00 2001 From: Erhnysr Date: Tue, 15 Sep 2026 15:52:20 +0300 Subject: [PATCH 2/3] chore: retrigger CI check From d49458018e67e06483a77924c3e60d73a5f16c7c Mon Sep 17 00:00:00 2001 From: Erhnysr Date: Sat, 19 Sep 2026 09:41:24 +0300 Subject: [PATCH 3/3] fix(keys): make isHexString prefix check case-insensitive isHexString now recognizes an uppercase "0X" prefix in addition to "0x", matching hexStringToBytes which already lower-cases the value before checking for the prefix. Both isHexString("0XABCDEF") and the bare isHexString("0X") now return true, so the two hex helpers accept the same prefix variants. This answers the "which prefix variants should be accepted" question raised in #161 that venables asked to resolve when closing #198. --- .changeset/is-hex-string-empty-body.md | 2 ++ packages/keys/src/encoding/hex.test.ts | 8 ++++++++ packages/keys/src/encoding/hex.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.changeset/is-hex-string-empty-body.md b/.changeset/is-hex-string-empty-body.md index ccde0b9a..de33f397 100644 --- a/.changeset/is-hex-string-empty-body.md +++ b/.changeset/is-hex-string-empty-body.md @@ -3,3 +3,5 @@ --- `isHexString` now returns `true` for a bare `0x` prefix with an empty body, matching its documented behavior. The check required at least one hex digit after the prefix, so `isHexString("0x")` returned `false` even though the JSDoc example states it returns `true`. A bare empty string with no prefix (`""`) continues to return `false`. + +`isHexString` also now accepts an uppercase `0X` prefix (e.g. `isHexString("0XABCDEF")` and `isHexString("0X")` both return `true`), making its prefix handling case-insensitive and consistent with `hexStringToBytes`, which already lower-cased the value before checking for the prefix. diff --git a/packages/keys/src/encoding/hex.test.ts b/packages/keys/src/encoding/hex.test.ts index 9977fa97..2e984c10 100644 --- a/packages/keys/src/encoding/hex.test.ts +++ b/packages/keys/src/encoding/hex.test.ts @@ -48,6 +48,14 @@ describe("isHexString", () => { expect(isHexString("0x")).toBe(true) }) + test("returns true for valid hex strings with an uppercase 0X prefix", () => { + expect(isHexString("0XABCDEF")).toBe(true) + }) + + test("returns true for a bare uppercase 0X prefix with an empty body", () => { + expect(isHexString("0X")).toBe(true) + }) + test("returns false for an empty string with no prefix", () => { expect(isHexString("")).toBe(false) }) diff --git a/packages/keys/src/encoding/hex.ts b/packages/keys/src/encoding/hex.ts index 4a084790..b53f22b0 100644 --- a/packages/keys/src/encoding/hex.ts +++ b/packages/keys/src/encoding/hex.ts @@ -47,7 +47,7 @@ export function isHexString(value: unknown): value is string { return false } - const hasPrefix = value.startsWith("0x") + const hasPrefix = value.toLowerCase().startsWith("0x") const hexWithoutPrefix = hasPrefix ? value.slice(2) : value // A bare "0x" prefix has an empty body and is a valid (zero-length) hex