Skip to content
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

Add address related utils #112

Merged
merged 9 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"@ethereumjs/tx": "^4.1.2",
"@noble/hashes": "^1.3.1",
"@types/debug": "^4.1.7",
"debug": "^4.3.4",
"semver": "^7.3.8",
Expand Down
61 changes: 61 additions & 0 deletions src/hex.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
Hex,
add0x,
assertIsHexString,
assertIsStrictHexString,
isValidChecksumAddress,
isHexString,
isStrictHexString,
isValidHexAddress,
remove0x,
} from './hex';

Expand Down Expand Up @@ -151,6 +154,64 @@ describe('assertIsStrictHexString', () => {
});
});

describe('isValidHexAddress', () => {
it.each([
'0x0000000000000000000000000000000000000000' as Hex,
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
])('returns true for a valid prefixed hex address', (hexString) => {
expect(isValidHexAddress(hexString)).toBe(true);
});

it.each([
'0000000000000000000000000000000000000000',
'd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
])('returns false for a valid non-prefixed hex address', (hexString) => {
// @ts-expect-error - testing invalid input
expect(isValidHexAddress(hexString)).toBe(false);
});

it.each([
'12345g',
'1234567890abcdefg',
'1234567890abcdefG',
'1234567890abcdefABCDEFg',
'1234567890abcdefABCDEF1234567890abcdefABCDEFg',
'0x',
'0x0',
'0x12345g',
'0x1234567890abcdefg',
'0x1234567890abcdefG',
'0x1234567890abcdefABCDEFg',
'0x1234567890abcdefABCDEF1234567890abcdefABCDEFg',
'0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045',
'0xCF5609B003B2776699EEA1233F7C82D5695CC9AA',
'0Xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
])('returns false for an invalid hex address', (hexString) => {
// @ts-expect-error - testing invalid input
expect(isValidHexAddress(hexString)).toBe(false);
});
});

describe('isValidChecksumAddress', () => {
it.each([
'0x0000000000000000000000000000000000000000' as Hex,
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
'0xCf5609B003B2776699eEA1233F7C82D5695cC9AA' as Hex,
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
'0x8617E340B3D01FA5F11F306F4090FD50E238070D' as Hex,
])('returns true for a valid checksum address', (hexString) => {
expect(isValidChecksumAddress(hexString)).toBe(true);
});

it.each([
'0xz' as Hex,
'0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045' as Hex,
'0xCF5609B003B2776699EEA1233F7C82D5695CC9AA' as Hex,
])('returns false for an invalid checksum address', (hexString) => {
expect(isValidChecksumAddress(hexString)).toBe(false);
});
});

describe('add0x', () => {
it('adds a 0x-prefix to a string', () => {
expect(add0x('12345')).toBe('0x12345');
Expand Down
54 changes: 54 additions & 0 deletions src/hex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
import { is, pattern, string, Struct } from 'superstruct';

import { assert } from './assert';
import { bytesToHex } from './bytes';

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

Expand All @@ -9,6 +11,14 @@ export const StrictHexStruct = pattern(string(), /^0x[0-9a-f]+$/iu) 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,
) as Struct<Hex, null>;

/**
* Check if a string is a valid hex string.
Expand Down Expand Up @@ -55,6 +65,50 @@ export function assertIsStrictHexString(value: unknown): asserts value is Hex {
);
}

/**
* Validate that the passed prefixed hex string is a valid 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)
);
}

/**
* 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 function isValidChecksumAddress(possibleChecksum: Hex) {
if (!is(possibleChecksum, HexChecksumAddressStruct)) {
return false;
}

const unPrefixed = remove0x(possibleChecksum);
const unPrefixedHash = remove0x(
bytesToHex(keccak256(unPrefixed.toLowerCase())),
);

for (let i = 0; i < unPrefixedHash.length; i++) {
const value = parseInt(unPrefixedHash[i] as string, 16);
if (
(value > 7 && unPrefixed[i]?.toUpperCase() !== unPrefixed[i]) ||
(value <= 7 && unPrefixed[i]?.toLowerCase() !== unPrefixed[i])
) {
return false;
}
}

return true;
}

/**
* Add the `0x`-prefix to a hexadecimal string. If the string already has the
* prefix, it is returned as-is.
Expand Down
10 changes: 9 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ __metadata:
"@metamask/eslint-config-jest": ^11.0.0
"@metamask/eslint-config-nodejs": ^11.0.1
"@metamask/eslint-config-typescript": ^11.0.0
"@noble/hashes": ^1.3.1
"@types/debug": ^4.1.7
"@types/jest": ^28.1.7
"@types/node": ^17.0.23
Expand Down Expand Up @@ -1108,13 +1109,20 @@ __metadata:
languageName: node
linkType: hard

"@noble/hashes@npm:1.3.0, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:~1.3.0":
"@noble/hashes@npm:1.3.0":
version: 1.3.0
resolution: "@noble/hashes@npm:1.3.0"
checksum: d7ddb6d7c60f1ce1f87facbbef5b724cdea536fc9e7f59ae96e0fc9de96c8f1a2ae2bdedbce10f7dcc621338dfef8533daa73c873f2b5c87fa1a4e05a95c2e2e
languageName: node
linkType: hard

"@noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0":
version: 1.3.1
resolution: "@noble/hashes@npm:1.3.1"
checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1
languageName: node
linkType: hard

"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
Expand Down