From f2f5c1686edde23f98dc50141aa465c6047e134d Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Mon, 10 Jul 2023 18:26:28 +0200 Subject: [PATCH] feat: validate checksum addresses --- package.json | 1 + src/hex.test.ts | 23 +++++++++++++++++++++++ src/hex.ts | 44 ++++++++++++++++++++++++++++++++++++++++++-- yarn.lock | 10 +++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7eb26aa85..80794aceb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/hex.test.ts b/src/hex.test.ts index efac6dfba..a191d7061 100644 --- a/src/hex.test.ts +++ b/src/hex.test.ts @@ -3,6 +3,7 @@ import { add0x, assertIsHexString, assertIsStrictHexString, + isChecksumAddress, isHexString, isStrictHexString, isValidHexAddress, @@ -182,6 +183,8 @@ describe('isValidHexAddress', () => { '0x1234567890abcdefG', '0x1234567890abcdefABCDEFg', '0x1234567890abcdefABCDEF1234567890abcdefABCDEFg', + '0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045', + '0xCF5609B003B2776699EEA1233F7C82D5695CC9AA', '0Xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', ])('returns false for an invalid hex address', (hexString) => { // @ts-expect-error - testing invalid input @@ -189,6 +192,26 @@ describe('isValidHexAddress', () => { }); }); +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'); diff --git a/src/hex.ts b/src/hex.ts index dbe30a39c..cbd3bfdab 100644 --- a/src/hex.ts +++ b/src/hex.ts @@ -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}`; @@ -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; +export const HexChecksumAddressStruct = pattern( string(), /^0x[0-9a-fA-F]{40}$/u, ) as Struct; @@ -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; } /** diff --git a/yarn.lock b/yarn.lock index 92902c418..b9ee57cc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -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"