Skip to content

Commit e358a03

Browse files
committed
chore: faster address validation
1 parent 33629cb commit e358a03

File tree

5 files changed

+177
-30
lines changed

5 files changed

+177
-30
lines changed

src/hex.test.ts

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
add0x,
44
assertIsHexString,
55
assertIsStrictHexString,
6-
isValidChecksumAddress,
6+
isValidChecksumAddressUnmemoized as isValidChecksumAddress,
77
isHexString,
8+
isHexAddress,
9+
isHexChecksumAddress,
810
isStrictHexString,
9-
isValidHexAddress,
11+
isValidHexAddressUnmemoized as isValidHexAddress,
1012
remove0x,
1113
getChecksumAddressUnmemoized as getChecksumAddress,
1214
getChecksumAddress as getChecksumAddressMemoized,
@@ -156,6 +158,100 @@ describe('assertIsStrictHexString', () => {
156158
});
157159
});
158160

161+
describe('isHexAddress', () => {
162+
it.each([
163+
'0x0000000000000000000000000000000000000000',
164+
'0x1234567890abcdef1234567890abcdef12345678',
165+
'0xffffffffffffffffffffffffffffffffffffffff',
166+
'0x0123456789abcdef0123456789abcdef01234567',
167+
])('returns true for a valid hex address', (hexString) => {
168+
expect(isHexAddress(hexString)).toBe(true);
169+
});
170+
171+
it.each([
172+
true,
173+
false,
174+
null,
175+
undefined,
176+
0,
177+
1,
178+
{},
179+
[],
180+
// Missing 0x prefix
181+
'0000000000000000000000000000000000000000',
182+
'1234567890abcdef1234567890abcdef12345678',
183+
// Wrong case prefix
184+
'0X1234567890abcdef1234567890abcdef12345678',
185+
// Too short
186+
'0x123456789abcdef1234567890abcdef1234567',
187+
'0x',
188+
'0x0',
189+
'0x123',
190+
// Too long
191+
'0x1234567890abcdef1234567890abcdef123456789',
192+
'0x1234567890abcdef1234567890abcdef12345678a',
193+
// Contains uppercase letters (should be lowercase only)
194+
'0x1234567890ABCDEF1234567890abcdef12345678',
195+
'0x1234567890abcdef1234567890ABCDEF12345678',
196+
'0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
197+
// Invalid characters
198+
'0x1234567890abcdefg123456789abcdef12345678',
199+
'0x1234567890abcdef1234567890abcdef1234567g',
200+
'0x1234567890abcdef123456789abcdef12345678!',
201+
])('returns false for an invalid hex address', (hexString) => {
202+
expect(isHexAddress(hexString)).toBe(false);
203+
});
204+
});
205+
206+
describe('isHexChecksumAddress', () => {
207+
it.each([
208+
'0x0000000000000000000000000000000000000000',
209+
'0x1234567890abcdef1234567890abcdef12345678',
210+
'0x1234567890ABCDEF1234567890abcdef12345678',
211+
'0x1234567890abcdef1234567890ABCDEF12345678',
212+
'0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
213+
'0xffffffffffffffffffffffffffffffffffffffff',
214+
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
215+
'0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed',
216+
'0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
217+
])('returns true for a valid hex checksum address', (hexString) => {
218+
expect(isHexChecksumAddress(hexString)).toBe(true);
219+
});
220+
221+
it.each([
222+
true,
223+
false,
224+
null,
225+
undefined,
226+
0,
227+
1,
228+
{},
229+
[],
230+
// Missing 0x prefix
231+
'0000000000000000000000000000000000000000',
232+
'1234567890abcdef1234567890abcdef12345678',
233+
'd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
234+
// Wrong case prefix
235+
'0X1234567890abcdef1234567890abcdef12345678',
236+
'0Xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
237+
// Too short
238+
'0x123456789abcdef1234567890abcdef1234567',
239+
'0x',
240+
'0x0',
241+
'0x123',
242+
// Too long
243+
'0x1234567890abcdef1234567890abcdef123456789',
244+
'0x1234567890abcdef1234567890abcdef12345678a',
245+
// Invalid characters
246+
'0x1234567890abcdefg123456789abcdef12345678',
247+
'0x1234567890abcdef1234567890abcdef1234567g',
248+
'0x1234567890abcdef123456789abcdef12345678!',
249+
'0x1234567890abcdef123456789abcdef12345678@',
250+
])('returns false for an invalid hex checksum address', (hexString) => {
251+
expect(isHexChecksumAddress(hexString)).toBe(false);
252+
});
253+
});
254+
159255
describe('isValidHexAddress', () => {
160256
it.each([
161257
'0x0000000000000000000000000000000000000000' as Hex,

src/hex.ts

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
1-
import type { Struct } from '@metamask/superstruct';
2-
import { is, pattern, string } from '@metamask/superstruct';
1+
import { pattern, type Struct, string } from '@metamask/superstruct';
32
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
43
import memoize from 'lodash.memoize';
54

65
import { assert } from './assert';
76

87
export type Hex = `0x${string}`;
98

10-
export const HexStruct = pattern(string(), /^(?:0x)?[0-9a-f]+$/iu);
11-
export const StrictHexStruct = pattern(string(), /^0x[0-9a-f]+$/iu) as Struct<
9+
// Use native regexes instead of superstruct for maximum performance.
10+
// Pre-compiled regex for maximum performance - avoids recompilation on each call
11+
const HEX_REGEX = /^(?:0x)?[0-9a-f]+$/iu;
12+
const STRICT_HEX_REGEX = /^0x[0-9a-f]+$/iu;
13+
const HEX_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/u;
14+
const HEX_CHECKSUM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/u;
15+
16+
export const HexStruct = pattern(string(), HEX_REGEX);
17+
export const StrictHexStruct = pattern(string(), STRICT_HEX_REGEX) as Struct<
18+
Hex,
19+
null
20+
>;
21+
export const HexAddressStruct = pattern(string(), HEX_ADDRESS_REGEX) as Struct<
1222
Hex,
1323
null
1424
>;
15-
export const HexAddressStruct = pattern(
16-
string(),
17-
/^0x[0-9a-f]{40}$/u,
18-
) as Struct<Hex, null>;
1925
export const HexChecksumAddressStruct = pattern(
2026
string(),
21-
/^0x[0-9a-fA-F]{40}$/u,
27+
HEX_CHECKSUM_ADDRESS_REGEX,
2228
) as Struct<Hex, null>;
2329

30+
const isString = (value: unknown): value is string => typeof value === 'string';
31+
2432
/**
2533
* Check if a string is a valid hex string.
2634
*
2735
* @param value - The value to check.
2836
* @returns Whether the value is a valid hex string.
2937
*/
3038
export function isHexString(value: unknown): value is string {
31-
return is(value, HexStruct);
39+
return isString(value) && HEX_REGEX.test(value);
3240
}
3341

3442
/**
@@ -39,7 +47,27 @@ export function isHexString(value: unknown): value is string {
3947
* @returns Whether the value is a valid hex string.
4048
*/
4149
export function isStrictHexString(value: unknown): value is Hex {
42-
return is(value, StrictHexStruct);
50+
return isString(value) && STRICT_HEX_REGEX.test(value);
51+
}
52+
53+
/**
54+
* Check if a string is a valid hex address.
55+
*
56+
* @param value - The value to check.
57+
* @returns Whether the value is a valid hex address.
58+
*/
59+
export function isHexAddress(value: unknown): value is Hex {
60+
return isString(value) && HEX_ADDRESS_REGEX.test(value);
61+
}
62+
63+
/**
64+
* Check if a string is a valid hex checksum address.
65+
*
66+
* @param value - The value to check.
67+
* @returns Whether the value is a valid hex checksum address.
68+
*/
69+
export function isHexChecksumAddress(value: unknown): value is Hex {
70+
return isString(value) && HEX_CHECKSUM_ADDRESS_REGEX.test(value);
4371
}
4472

4573
/**
@@ -66,20 +94,6 @@ export function assertIsStrictHexString(value: unknown): asserts value is Hex {
6694
);
6795
}
6896

69-
/**
70-
* Validate that the passed prefixed hex string is an all-lowercase
71-
* hex address, or a valid mixed-case checksum address.
72-
*
73-
* @param possibleAddress - Input parameter to check against.
74-
* @returns Whether or not the input is a valid hex address.
75-
*/
76-
export function isValidHexAddress(possibleAddress: Hex) {
77-
return (
78-
is(possibleAddress, HexAddressStruct) ||
79-
isValidChecksumAddress(possibleAddress)
80-
);
81-
}
82-
8397
/**
8498
* Encode a passed hex string as an ERC-55 mixed-case checksum address.
8599
* This is the unmemoized version, primarily used for testing.
@@ -89,7 +103,7 @@ export function isValidHexAddress(possibleAddress: Hex) {
89103
* @see https://eips.ethereum.org/EIPS/eip-55
90104
*/
91105
export function getChecksumAddressUnmemoized(hexAddress: Hex): Hex {
92-
assert(is(hexAddress, HexChecksumAddressStruct), 'Invalid hex address.');
106+
assert(isHexChecksumAddress(hexAddress), 'Invalid hex address.');
93107
const address = remove0x(hexAddress).toLowerCase();
94108

95109
const hashBytes = keccak256(address);
@@ -127,14 +141,45 @@ export const getChecksumAddress = memoize(getChecksumAddressUnmemoized);
127141
* @param possibleChecksum - The hex address to check.
128142
* @returns True if the address is a checksum address.
129143
*/
130-
export function isValidChecksumAddress(possibleChecksum: Hex) {
131-
if (!is(possibleChecksum, HexChecksumAddressStruct)) {
144+
export function isValidChecksumAddressUnmemoized(possibleChecksum: Hex) {
145+
if (!isHexChecksumAddress(possibleChecksum)) {
132146
return false;
133147
}
134148

135149
return getChecksumAddress(possibleChecksum) === possibleChecksum;
136150
}
137151

152+
/**
153+
* Validate that the passed hex string is a valid ERC-55 mixed-case
154+
* checksum address.
155+
*
156+
* @param possibleChecksum - The hex address to check.
157+
* @returns True if the address is a checksum address.
158+
*/
159+
export const isValidChecksumAddress = memoize(isValidChecksumAddressUnmemoized);
160+
161+
/**
162+
* Validate that the passed prefixed hex string is an all-lowercase
163+
* hex address, or a valid mixed-case checksum address.
164+
*
165+
* @param possibleAddress - Input parameter to check against.
166+
* @returns Whether or not the input is a valid hex address.
167+
*/
168+
export function isValidHexAddressUnmemoized(possibleAddress: Hex) {
169+
return (
170+
isHexAddress(possibleAddress) || isValidChecksumAddress(possibleAddress)
171+
);
172+
}
173+
174+
/**
175+
* Validate that the passed prefixed hex string is an all-lowercase
176+
* hex address, or a valid mixed-case checksum address.
177+
*
178+
* @param possibleAddress - Input parameter to check against.
179+
* @returns Whether or not the input is a valid hex address.
180+
*/
181+
export const isValidHexAddress = memoize(isValidHexAddressUnmemoized);
182+
138183
/**
139184
* Add the `0x`-prefix to a hexadecimal string. If the string already has the
140185
* prefix, it is returned as-is.

src/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ describe('index', () => {
118118
"isErrorWithCode",
119119
"isErrorWithMessage",
120120
"isErrorWithStack",
121+
"isHexAddress",
122+
"isHexChecksumAddress",
121123
"isHexString",
122124
"isJsonRpcError",
123125
"isJsonRpcFailure",

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export {
1515
HexChecksumAddressStruct,
1616
isHexString,
1717
isStrictHexString,
18+
isHexAddress,
19+
isHexChecksumAddress,
1820
assertIsHexString,
1921
assertIsStrictHexString,
2022
isValidHexAddress,

src/node.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ describe('node', () => {
123123
"isErrorWithCode",
124124
"isErrorWithMessage",
125125
"isErrorWithStack",
126+
"isHexAddress",
127+
"isHexChecksumAddress",
126128
"isHexString",
127129
"isJsonRpcError",
128130
"isJsonRpcFailure",

0 commit comments

Comments
 (0)