Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions src/hex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
101 changes: 73 additions & 28 deletions src/hex.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
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';

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<Hex, null>;
export const HexChecksumAddressStruct = pattern(
string(),
/^0x[0-9a-fA-F]{40}$/u,
HEX_CHECKSUM_ADDRESS_REGEX,
) as Struct<Hex, null>;

const isString = (value: unknown): value is string => typeof value === 'string';

/**
* Check if a string is a valid hex string.
*
* @param value - The value to check.
* @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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ describe('index', () => {
"isErrorWithCode",
"isErrorWithMessage",
"isErrorWithStack",
"isHexAddress",
"isHexChecksumAddress",
"isHexString",
"isJsonRpcError",
"isJsonRpcFailure",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export {
HexChecksumAddressStruct,
isHexString,
isStrictHexString,
isHexAddress,
isHexChecksumAddress,
assertIsHexString,
assertIsStrictHexString,
isValidHexAddress,
Expand Down
2 changes: 2 additions & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ describe('node', () => {
"isErrorWithCode",
"isErrorWithMessage",
"isErrorWithStack",
"isHexAddress",
"isHexChecksumAddress",
"isHexString",
"isJsonRpcError",
"isJsonRpcFailure",
Expand Down
Loading