-
Notifications
You must be signed in to change notification settings - Fork 15
Normalize labelhash for ENSRainbow Heal request #347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d992400
f280f06
c1c372a
b9edb4d
baf003f
5a4074e
f0c1dbf
daadcc8
ce49fc4
be4806f
c68715e
f7bb4bf
0a8d099
628e8ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@ensnode/ensrainbow-sdk": minor | ||
| "ensindexer": patch | ||
| --- | ||
|
|
||
| Normalize labelhash for ENSRainbow Heal request |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| export * from "./client"; | ||
| export * from "./consts"; | ||
| export * from "./label-utils"; | ||
| export * from "./utils"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { | ||
| EncodedLabelhash, | ||
| InvalidLabelhashError, | ||
| parseEncodedLabelhash, | ||
| parseLabelhash, | ||
| } from "./utils"; | ||
|
|
||
| describe("parseLabelhash", () => { | ||
| it("should normalize a valid labelhash", () => { | ||
| // 64 zeros | ||
| expect(parseLabelhash("0000000000000000000000000000000000000000000000000000000000000000")).toBe( | ||
| "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| ); | ||
|
|
||
| // 64 zeros with 0x prefix | ||
| expect( | ||
| parseLabelhash("0x0000000000000000000000000000000000000000000000000000000000000000"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 63 zeros | ||
| expect(parseLabelhash("000000000000000000000000000000000000000000000000000000000000000")).toBe( | ||
| "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| ); | ||
|
|
||
| // 63 zeros with 0x prefix | ||
| expect( | ||
| parseLabelhash("0x000000000000000000000000000000000000000000000000000000000000000"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 64 characters | ||
| expect(parseLabelhash("A000000000000000000000000000000000000000000000000000000000000000")).toBe( | ||
| "0xa000000000000000000000000000000000000000000000000000000000000000", | ||
| ); | ||
|
|
||
| // 63 characters | ||
| expect(parseLabelhash("A00000000000000000000000000000000000000000000000000000000000000")).toBe( | ||
| "0x0a00000000000000000000000000000000000000000000000000000000000000", | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw for invalid labelhash", () => { | ||
| // Invalid characters | ||
| expect(() => | ||
| parseLabelhash("0xG000000000000000000000000000000000000000000000000000000000000000"), | ||
| ).toThrow(InvalidLabelhashError); | ||
|
|
||
| // Too short | ||
| expect(() => parseLabelhash("0x00000")).toThrow(InvalidLabelhashError); | ||
|
|
||
| // Too long | ||
| expect(() => | ||
| parseLabelhash("0x00000000000000000000000000000000000000000000000000000000000000000"), | ||
| ).toThrow(InvalidLabelhashError); | ||
| }); | ||
| }); | ||
|
|
||
| describe("parseEncodedLabelhash", () => { | ||
| it("should normalize a valid encoded labelhash", () => { | ||
| // 64 zeros | ||
| expect( | ||
| parseEncodedLabelhash("[0000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 64 zeros with 0x prefix | ||
| expect( | ||
| parseEncodedLabelhash("[0x0000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 63 zeros | ||
| expect( | ||
| parseEncodedLabelhash("[000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 63 zeros with 0x prefix | ||
| expect( | ||
| parseEncodedLabelhash("[0x000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 64 characters | ||
| expect( | ||
| parseEncodedLabelhash("[A000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0xa000000000000000000000000000000000000000000000000000000000000000"); | ||
|
|
||
| // 64 characters with 0x prefix | ||
| expect( | ||
| parseEncodedLabelhash("[A00000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toBe("0x0a00000000000000000000000000000000000000000000000000000000000000"); | ||
| }); | ||
|
|
||
| it("should throw for invalid encoded labelhash", () => { | ||
| // Not enclosed in brackets | ||
| expect(() => | ||
| parseEncodedLabelhash( | ||
| "0000000000000000000000000000000000000000000000000000000000000000" as EncodedLabelhash, | ||
| ), | ||
| ).toThrow(InvalidLabelhashError); | ||
| expect(() => | ||
| parseEncodedLabelhash( | ||
| "[0000000000000000000000000000000000000000000000000000000000000000" as EncodedLabelhash, | ||
| ), | ||
| ).toThrow(InvalidLabelhashError); | ||
| expect(() => | ||
| parseEncodedLabelhash( | ||
| "0000000000000000000000000000000000000000000000000000000000000000]" as EncodedLabelhash, | ||
| ), | ||
| ).toThrow(InvalidLabelhashError); | ||
|
|
||
| // 62 zeros - too short | ||
| expect(() => | ||
| parseEncodedLabelhash("[00000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toThrow(InvalidLabelhashError); | ||
|
|
||
| // 65 zeros - too long | ||
| expect(() => | ||
| parseEncodedLabelhash("[00000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toThrow(InvalidLabelhashError); | ||
|
|
||
| // wrong 0X prefix | ||
| expect(() => | ||
| parseEncodedLabelhash("[0X0000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toThrow(InvalidLabelhashError); | ||
|
|
||
| // Invalid content | ||
| expect(() => parseEncodedLabelhash("[00000]")).toThrow(InvalidLabelhashError); | ||
| expect(() => | ||
| parseEncodedLabelhash("[0xG000000000000000000000000000000000000000000000000000000000000000]"), | ||
| ).toThrow(InvalidLabelhashError); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import type { Labelhash } from "@ensnode/utils/types"; | ||
| import { Hex } from "viem"; | ||
| import { isHex } from "viem/utils"; | ||
|
|
||
| export type EncodedLabelhash = `[${string}]`; | ||
|
|
||
| /** | ||
| * Error thrown when a labelhash cannot be normalized. | ||
| */ | ||
| export class InvalidLabelhashError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = "InvalidLabelhashError"; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Parses a labelhash string and normalizes it to the format expected by the ENSRainbow API. | ||
| * If the input labelhash is 63 characters long, a leading zero will be added to make it 64 characters. | ||
| * | ||
| * @param maybeLabelhash - The string to parse as a labelhash | ||
| * @returns A normalized labelhash (a 0x-prefixed, lowercased, 64-character hex string) | ||
| * @throws {InvalidLabelhashError} If the input cannot be normalized to a valid labelhash | ||
| */ | ||
| export function parseLabelhash(maybeLabelhash: string): Labelhash { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo we should refactor this entire file to just: function parseLabelhashOrEncodedLabelhash(maybeLabelHash: string) {
// 1. ensure is Hex formatted
let hexLabelhash: Hex;
try {
// attempt to decode label hash with ensjs
// NOTE: decodeLabelhash will throw InvalidEncodedLabelError or return hex-looking string
hexLabelhash = decodeLabelhash(maybeLabelHash);
} catch {
// if not encoded label hash, prefix with 0x
hexLabelhash = maybeLabelHash.startsWith("0x")
? (maybeLabelHash as `0x${string}`)
: `0x${maybeLabelHash}`;
}
// 2. if not hex, throw
if (!isHex(hexLabelhash, { strict: true })) throw new InvalidLabelhashError();
// 3. ensure hex part is correctly sized, padding left
const normalizedHex = pad(hexLabelhash, { dir: "left", size: 32 });
if (size(normalizedHex) !== 32) throw new InvalidLabelhashError();
// 4. return lower case string
return normalizedHex.toLowerCase() as Labelhash;
}
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if ensjs isn't already a dependency, we can keep
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and the logic can be separated into multiple functions if desired. but we shouldn't be re-implementing hex-related logic that's already available in viem
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have ensjs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated the code to use |
||
| // Remove 0x prefix if present | ||
| let hexPart = maybeLabelhash.startsWith("0x") ? maybeLabelhash.slice(2) : maybeLabelhash; | ||
|
|
||
| // Check if the correct number of bytes (32 bytes = 64 hex chars) | ||
| // If length is 63, pad with a leading zero to make it 64 | ||
| if (hexPart.length == 63) { | ||
| hexPart = `0${hexPart}`; | ||
| } else if (hexPart.length !== 64) { | ||
| throw new InvalidLabelhashError( | ||
| `Invalid labelhash length: expected 32 bytes (64 hex chars), got ${hexPart.length / 2} bytes: ${maybeLabelhash}`, | ||
| ); | ||
| } | ||
| const normalizedHex: Hex = `0x${hexPart}`; | ||
|
|
||
| // Check if all characters are valid hex digits | ||
| if (!isHex(normalizedHex, { strict: true })) { | ||
| throw new InvalidLabelhashError( | ||
| `Invalid labelhash: contains non-hex characters: ${maybeLabelhash}`, | ||
| ); | ||
| } | ||
|
|
||
| // Ensure lowercase | ||
| return normalizedHex.toLowerCase() as Labelhash; | ||
| } | ||
|
|
||
| /** | ||
| * Parses an encoded labelhash string (surrounded by square brackets) and normalizes it. | ||
| * | ||
| * @param maybeEncodedLabelhash - The string to parse as an encoded labelhash | ||
| * @returns A normalized labelhash (a 0x-prefixed, lowercased, 64-character hex string) | ||
| * @throws {InvalidLabelhashError} If the input is not properly encoded or cannot be normalized | ||
| */ | ||
| export function parseEncodedLabelhash(maybeEncodedLabelhash: EncodedLabelhash): Labelhash { | ||
| // Check if the string is enclosed in square brackets | ||
| if (!maybeEncodedLabelhash.startsWith("[") || !maybeEncodedLabelhash.endsWith("]")) { | ||
| throw new InvalidLabelhashError( | ||
| `Invalid encoded labelhash: must be enclosed in square brackets: ${maybeEncodedLabelhash}`, | ||
| ); | ||
| } | ||
|
|
||
| // Remove the square brackets and parse as a regular labelhash | ||
| const innerValue = maybeEncodedLabelhash.slice(1, -1); | ||
| return parseLabelhash(innerValue); | ||
| } | ||
|
|
||
| /** | ||
| * Parses a labelhash or encoded labelhash string and normalizes it to the format expected by the ENSRainbow API. | ||
| * | ||
| * @param maybeLabelhash - The string to parse as a labelhash | ||
| * @returns A normalized labelhash (a 0x-prefixed, lowercased, 64-character hex string) | ||
| * @throws {InvalidLabelhashError} If the input cannot be normalized to a valid labelhash | ||
| */ | ||
| export function parseLabelhashOrEncodedLabelhash(maybeLabelhash: string): Labelhash { | ||
| if (maybeLabelhash.startsWith("[") && maybeLabelhash.endsWith("]")) { | ||
| return parseEncodedLabelhash(maybeLabelhash as EncodedLabelhash); | ||
| } else { | ||
| return parseLabelhash(maybeLabelhash); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or this should be just
labelhash: string?