diff --git a/packages/hash/src.ts/index.ts b/packages/hash/src.ts/index.ts index ca9de9e7b5..6efbd8d7f1 100644 --- a/packages/hash/src.ts/index.ts +++ b/packages/hash/src.ts/index.ts @@ -4,7 +4,7 @@ import { id } from "./id"; import { dnsEncode, isValidName, namehash } from "./namehash"; import { hashMessage, messagePrefix } from "./message"; -import { ens_normalize as ensNormalize } from "./ens-normalize/lib"; +import { ensNormalize } from "./namehash"; import { TypedDataEncoder as _TypedDataEncoder } from "./typed-data"; diff --git a/packages/hash/src.ts/namehash.ts b/packages/hash/src.ts/namehash.ts index 14fe7aef52..ded377e0d4 100644 --- a/packages/hash/src.ts/namehash.ts +++ b/packages/hash/src.ts/namehash.ts @@ -1,5 +1,5 @@ import { concat, hexlify } from "@ethersproject/bytes"; -import { toUtf8Bytes } from "@ethersproject/strings"; +import { toUtf8Bytes, toUtf8String } from "@ethersproject/strings"; import { keccak256 } from "@ethersproject/keccak256"; import { Logger } from "@ethersproject/logger"; @@ -11,11 +11,59 @@ import { ens_normalize } from "./ens-normalize/lib"; const Zeros = new Uint8Array(32); Zeros.fill(0); -const Partition = new RegExp("^((.*)\\.)?([^.]+)$"); +function checkComponent(comp: Uint8Array): Uint8Array { + if (comp.length === 0) { throw new Error("invalid ENS name; empty component"); } + let nonUnder = false; + let last = -1; + for (let i = 0; i < comp.length; i++) { + const c = comp[i]; + + // An underscore (i.e. "_"); only allows at the beginning + if (c === 0x5f) { + if (nonUnder) { throw new Error("invalid ENS name; non-prefix underscore"); } + } else { + // A hyphen (i.e. "-"); only allows a single in a row + if (c === 0x2d && last === c) { + throw new Error("invalid ENS name; double-hyphen"); + } + nonUnder = true; + } + last = c; + } + return comp; +} + +function ensNameSplit(name: string): Array { + const bytes = toUtf8Bytes(ens_normalize(name)); + const comps: Array = [ ]; + + if (name.length === 0) { return comps; } + + let last = 0; + for (let i = 0; i < bytes.length; i++) { + const d = bytes[i]; + + // A separator (i.e. "."); copy this component + if (d === 0x2e) { + comps.push(checkComponent(bytes.slice(last, i))); + last = i + 1; + } + } + + // There was a stray separator at the end of the name + if (last >= bytes.length) { throw new Error("invalid ENS name; empty component"); } + + comps.push(checkComponent(bytes.slice(last))); + return comps; +} + +export function ensNormalize(name: string): string { + return ensNameSplit(name).map((comp) => toUtf8String(comp)).join("."); +} export function isValidName(name: string): boolean { try { - return ens_normalize(name).length !== 0; + return (ensNameSplit(name).length !== 0); } catch (error) { } return false; } @@ -26,34 +74,28 @@ export function namehash(name: string): string { logger.throwArgumentError("invalid ENS name; not a string", "name", name); } - let current = ens_normalize(name); let result: string | Uint8Array = Zeros; - while (current.length) { - const partition = current.match(Partition); - if (partition == null || partition[2] === "") { - logger.throwArgumentError("invalid ENS address; missing component", "name", name); - } - const label = toUtf8Bytes(partition[3]); - result = keccak256(concat([result, keccak256(label)])); - current = partition[2] || ""; + const comps = ensNameSplit(name); + while (comps.length) { + result = keccak256(concat([result, keccak256(comps.pop())])); } return hexlify(result); } export function dnsEncode(name: string): string { - name = ens_normalize(name) - return hexlify(concat(name.split(".").map((comp) => { - + return hexlify(concat(ensNameSplit(name).map((comp) => { // DNS does not allow components over 63 bytes in length - if (toUtf8Bytes(comp).length > 63) { + if (comp.length > 63) { throw new Error("invalid DNS encoded entry; length exceeds 63 bytes"); } - // We jam in an _ prefix to fill in with the length later - const bytes = toUtf8Bytes("_" + comp); + const bytes = new Uint8Array(comp.length + 1); + bytes.set(comp, 1); bytes[0] = bytes.length - 1; return bytes; + }))) + "00"; } +