From e358a03d91ed7e453d78c552a5d1f9215867ca11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Such=C3=BD?= Date: Tue, 1 Jul 2025 16:23:53 +0200 Subject: [PATCH] chore: faster address validation --- src/hex.test.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++- src/hex.ts | 101 +++++++++++++++++++++++++++++++++------------- src/index.test.ts | 2 + src/index.ts | 2 + src/node.test.ts | 2 + 5 files changed, 177 insertions(+), 30 deletions(-) diff --git a/src/hex.test.ts b/src/hex.test.ts index e6a2d4979..4a83f54f5 100644 --- a/src/hex.test.ts +++ b/src/hex.test.ts @@ -3,10 +3,12 @@ import { add0x, assertIsHexString, assertIsStrictHexString, - isValidChecksumAddress, + isValidChecksumAddressUnmemoized as isValidChecksumAddress, isHexString, + isHexAddress, + isHexChecksumAddress, isStrictHexString, - isValidHexAddress, + isValidHexAddressUnmemoized as isValidHexAddress, remove0x, getChecksumAddressUnmemoized as getChecksumAddress, getChecksumAddress as getChecksumAddressMemoized, @@ -156,6 +158,100 @@ describe('assertIsStrictHexString', () => { }); }); +describe('isHexAddress', () => { + it.each([ + '0x0000000000000000000000000000000000000000', + '0x1234567890abcdef1234567890abcdef12345678', + '0xffffffffffffffffffffffffffffffffffffffff', + '0x0123456789abcdef0123456789abcdef01234567', + ])('returns true for a valid hex address', (hexString) => { + expect(isHexAddress(hexString)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 0, + 1, + {}, + [], + // Missing 0x prefix + '0000000000000000000000000000000000000000', + '1234567890abcdef1234567890abcdef12345678', + // Wrong case prefix + '0X1234567890abcdef1234567890abcdef12345678', + // Too short + '0x123456789abcdef1234567890abcdef1234567', + '0x', + '0x0', + '0x123', + // Too long + '0x1234567890abcdef1234567890abcdef123456789', + '0x1234567890abcdef1234567890abcdef12345678a', + // Contains uppercase letters (should be lowercase only) + '0x1234567890ABCDEF1234567890abcdef12345678', + '0x1234567890abcdef1234567890ABCDEF12345678', + '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + // Invalid characters + '0x1234567890abcdefg123456789abcdef12345678', + '0x1234567890abcdef1234567890abcdef1234567g', + '0x1234567890abcdef123456789abcdef12345678!', + ])('returns false for an invalid hex address', (hexString) => { + expect(isHexAddress(hexString)).toBe(false); + }); +}); + +describe('isHexChecksumAddress', () => { + it.each([ + '0x0000000000000000000000000000000000000000', + '0x1234567890abcdef1234567890abcdef12345678', + '0x1234567890ABCDEF1234567890abcdef12345678', + '0x1234567890abcdef1234567890ABCDEF12345678', + '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + '0xffffffffffffffffffffffffffffffffffffffff', + '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', + '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', + ])('returns true for a valid hex checksum address', (hexString) => { + expect(isHexChecksumAddress(hexString)).toBe(true); + }); + + it.each([ + true, + false, + null, + undefined, + 0, + 1, + {}, + [], + // Missing 0x prefix + '0000000000000000000000000000000000000000', + '1234567890abcdef1234567890abcdef12345678', + 'd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + // Wrong case prefix + '0X1234567890abcdef1234567890abcdef12345678', + '0Xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + // Too short + '0x123456789abcdef1234567890abcdef1234567', + '0x', + '0x0', + '0x123', + // Too long + '0x1234567890abcdef1234567890abcdef123456789', + '0x1234567890abcdef1234567890abcdef12345678a', + // Invalid characters + '0x1234567890abcdefg123456789abcdef12345678', + '0x1234567890abcdef1234567890abcdef1234567g', + '0x1234567890abcdef123456789abcdef12345678!', + '0x1234567890abcdef123456789abcdef12345678@', + ])('returns false for an invalid hex checksum address', (hexString) => { + expect(isHexChecksumAddress(hexString)).toBe(false); + }); +}); + describe('isValidHexAddress', () => { it.each([ '0x0000000000000000000000000000000000000000' as Hex, diff --git a/src/hex.ts b/src/hex.ts index 2664f07d4..d7dc2ec37 100644 --- a/src/hex.ts +++ b/src/hex.ts @@ -1,5 +1,4 @@ -import type { Struct } from '@metamask/superstruct'; -import { is, pattern, string } from '@metamask/superstruct'; +import { pattern, type Struct, string } from '@metamask/superstruct'; import { keccak_256 as keccak256 } from '@noble/hashes/sha3'; import memoize from 'lodash.memoize'; @@ -7,20 +6,29 @@ import { assert } from './assert'; export type Hex = `0x${string}`; -export const HexStruct = pattern(string(), /^(?:0x)?[0-9a-f]+$/iu); -export const StrictHexStruct = pattern(string(), /^0x[0-9a-f]+$/iu) as Struct< +// Use native regexes instead of superstruct for maximum performance. +// Pre-compiled regex for maximum performance - avoids recompilation on each call +const HEX_REGEX = /^(?:0x)?[0-9a-f]+$/iu; +const STRICT_HEX_REGEX = /^0x[0-9a-f]+$/iu; +const HEX_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/u; +const HEX_CHECKSUM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/u; + +export const HexStruct = pattern(string(), HEX_REGEX); +export const StrictHexStruct = pattern(string(), STRICT_HEX_REGEX) as Struct< + Hex, + null +>; +export const HexAddressStruct = pattern(string(), HEX_ADDRESS_REGEX) as Struct< Hex, null >; -export const HexAddressStruct = pattern( - string(), - /^0x[0-9a-f]{40}$/u, -) as Struct; export const HexChecksumAddressStruct = pattern( string(), - /^0x[0-9a-fA-F]{40}$/u, + HEX_CHECKSUM_ADDRESS_REGEX, ) as Struct; +const isString = (value: unknown): value is string => typeof value === 'string'; + /** * Check if a string is a valid hex string. * @@ -28,7 +36,7 @@ export const HexChecksumAddressStruct = pattern( * @returns Whether the value is a valid hex string. */ export function isHexString(value: unknown): value is string { - return is(value, HexStruct); + return isString(value) && HEX_REGEX.test(value); } /** @@ -39,7 +47,27 @@ export function isHexString(value: unknown): value is string { * @returns Whether the value is a valid hex string. */ export function isStrictHexString(value: unknown): value is Hex { - return is(value, StrictHexStruct); + return isString(value) && STRICT_HEX_REGEX.test(value); +} + +/** + * Check if a string is a valid hex address. + * + * @param value - The value to check. + * @returns Whether the value is a valid hex address. + */ +export function isHexAddress(value: unknown): value is Hex { + return isString(value) && HEX_ADDRESS_REGEX.test(value); +} + +/** + * Check if a string is a valid hex checksum address. + * + * @param value - The value to check. + * @returns Whether the value is a valid hex checksum address. + */ +export function isHexChecksumAddress(value: unknown): value is Hex { + return isString(value) && HEX_CHECKSUM_ADDRESS_REGEX.test(value); } /** @@ -66,20 +94,6 @@ export function assertIsStrictHexString(value: unknown): asserts value is Hex { ); } -/** - * Validate that the passed prefixed hex string is an all-lowercase - * hex address, or a valid mixed-case checksum address. - * - * @param possibleAddress - Input parameter to check against. - * @returns Whether or not the input is a valid hex address. - */ -export function isValidHexAddress(possibleAddress: Hex) { - return ( - is(possibleAddress, HexAddressStruct) || - isValidChecksumAddress(possibleAddress) - ); -} - /** * Encode a passed hex string as an ERC-55 mixed-case checksum address. * This is the unmemoized version, primarily used for testing. @@ -89,7 +103,7 @@ export function isValidHexAddress(possibleAddress: Hex) { * @see https://eips.ethereum.org/EIPS/eip-55 */ export function getChecksumAddressUnmemoized(hexAddress: Hex): Hex { - assert(is(hexAddress, HexChecksumAddressStruct), 'Invalid hex address.'); + assert(isHexChecksumAddress(hexAddress), 'Invalid hex address.'); const address = remove0x(hexAddress).toLowerCase(); const hashBytes = keccak256(address); @@ -127,14 +141,45 @@ export const getChecksumAddress = memoize(getChecksumAddressUnmemoized); * @param possibleChecksum - The hex address to check. * @returns True if the address is a checksum address. */ -export function isValidChecksumAddress(possibleChecksum: Hex) { - if (!is(possibleChecksum, HexChecksumAddressStruct)) { +export function isValidChecksumAddressUnmemoized(possibleChecksum: Hex) { + if (!isHexChecksumAddress(possibleChecksum)) { return false; } return getChecksumAddress(possibleChecksum) === possibleChecksum; } +/** + * Validate that the passed hex string is a valid ERC-55 mixed-case + * checksum address. + * + * @param possibleChecksum - The hex address to check. + * @returns True if the address is a checksum address. + */ +export const isValidChecksumAddress = memoize(isValidChecksumAddressUnmemoized); + +/** + * Validate that the passed prefixed hex string is an all-lowercase + * hex address, or a valid mixed-case checksum address. + * + * @param possibleAddress - Input parameter to check against. + * @returns Whether or not the input is a valid hex address. + */ +export function isValidHexAddressUnmemoized(possibleAddress: Hex) { + return ( + isHexAddress(possibleAddress) || isValidChecksumAddress(possibleAddress) + ); +} + +/** + * Validate that the passed prefixed hex string is an all-lowercase + * hex address, or a valid mixed-case checksum address. + * + * @param possibleAddress - Input parameter to check against. + * @returns Whether or not the input is a valid hex address. + */ +export const isValidHexAddress = memoize(isValidHexAddressUnmemoized); + /** * Add the `0x`-prefix to a hexadecimal string. If the string already has the * prefix, it is returned as-is. diff --git a/src/index.test.ts b/src/index.test.ts index c3402921c..1db2b4793 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -118,6 +118,8 @@ describe('index', () => { "isErrorWithCode", "isErrorWithMessage", "isErrorWithStack", + "isHexAddress", + "isHexChecksumAddress", "isHexString", "isJsonRpcError", "isJsonRpcFailure", diff --git a/src/index.ts b/src/index.ts index 1f8c6c24d..9eae3660c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,8 @@ export { HexChecksumAddressStruct, isHexString, isStrictHexString, + isHexAddress, + isHexChecksumAddress, assertIsHexString, assertIsStrictHexString, isValidHexAddress, diff --git a/src/node.test.ts b/src/node.test.ts index 3808fd475..4e1e8727e 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -123,6 +123,8 @@ describe('node', () => { "isErrorWithCode", "isErrorWithMessage", "isErrorWithStack", + "isHexAddress", + "isHexChecksumAddress", "isHexString", "isJsonRpcError", "isJsonRpcFailure",