Skip to content

Commit

Permalink
feat: validate checksum addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
mikesposito committed Jul 10, 2023
1 parent d421bb7 commit f2f5c16
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 3 deletions.
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
23 changes: 23 additions & 0 deletions src/hex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
add0x,
assertIsHexString,
assertIsStrictHexString,
isChecksumAddress,
isHexString,
isStrictHexString,
isValidHexAddress,
Expand Down Expand Up @@ -182,13 +183,35 @@ describe('isValidHexAddress', () => {
'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('isChecksumAddress', () => {
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(isChecksumAddress(hexString)).toBe(true);
});

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

describe('add0x', () => {
it('adds a 0x-prefix to a string', () => {
expect(add0x('12345')).toBe('0x12345');
Expand Down
44 changes: 42 additions & 2 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 @@ -10,6 +12,10 @@ export const StrictHexStruct = pattern(string(), /^0x[0-9a-f]+$/iu) as Struct<
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>;
Expand Down Expand Up @@ -60,13 +66,47 @@ export function assertIsStrictHexString(value: unknown): asserts value is Hex {
}

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

/**
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

0 comments on commit f2f5c16

Please sign in to comment.