Skip to content

Commit

Permalink
Add address related utils (#112)
Browse files Browse the repository at this point in the history
* feat: add isValidHexAddress function

* fix: accept only prefixed addresses

* fix: remove case insensitive flag

* test: add test case for 0X

* feat: validate checksum addresses

* feat: break out address checksum encoding to erc55EncodeAddress function (#113)

* feat: break out address checksum encoding to erc55EncodeAddress function

* deps: [email protected]>2.1.0; dedupe @noble/hashes

* Update jsdoc

Co-authored-by: Maarten Zuidhoorn <[email protected]>

* refactor: rename erc55EncodeAddress function

* test: add test cases for getChecksumAddress

---------

Co-authored-by: Maarten Zuidhoorn <[email protected]>
Co-authored-by: Michele Esposito <[email protected]>

* docs: edit isValidHexAddress jsdoc description

* Update src/hex.ts

Co-authored-by: Mark Stacey <[email protected]>

* fix: add validation assertions

---------

Co-authored-by: legobeat <[email protected]>
Co-authored-by: Maarten Zuidhoorn <[email protected]>
Co-authored-by: Mark Stacey <[email protected]>
  • Loading branch information
4 people authored Jul 11, 2023
1 parent 47b757d commit f1024ca
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 26 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
90 changes: 90 additions & 0 deletions src/hex.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {
Hex,
add0x,
assertIsHexString,
assertIsStrictHexString,
isValidChecksumAddress,
isHexString,
isStrictHexString,
isValidHexAddress,
remove0x,
getChecksumAddress,
} from './hex';

describe('isHexString', () => {
Expand Down Expand Up @@ -151,6 +155,92 @@ 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('getChecksumAddress', () => {
it('returns the checksum address for a valid hex address', () => {
expect(
getChecksumAddress('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed'),
).toBe('0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed');

expect(
getChecksumAddress('0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359'),
).toBe('0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359');

expect(
getChecksumAddress('0x52908400098527886e0f7030069857d2e4169ee7'),
).toBe('0x52908400098527886E0F7030069857D2E4169EE7');

expect(
getChecksumAddress('0xde709f2102306220921060314715629080e2fb77'),
).toBe('0xde709f2102306220921060314715629080e2fb77');

expect(
getChecksumAddress('0x0000000000000000000000000000000000000000'),
).toBe('0x0000000000000000000000000000000000000000');
});

it('throws for an invalid hex address', () => {
expect(() => getChecksumAddress('0x')).toThrow('Invalid hex address.');
});
});

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
62 changes: 62 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,58 @@ 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.
*
* @param address - The hex address to encode.
* @returns The address encoded according to ERC-55.
* @see https://eips.ethereum.org/EIPS/eip-55
*/
export function getChecksumAddress(address: Hex) {
assert(is(address, HexChecksumAddressStruct), 'Invalid hex address.');
const unPrefixed = remove0x(address.toLowerCase());
const unPrefixedHash = remove0x(bytesToHex(keccak256(unPrefixed)));
return `0x${unPrefixed
.split('')
.map((character, nibbleIndex) => {
const hashCharacter = unPrefixedHash[nibbleIndex];
assert(is(hashCharacter, string()), 'Hash shorter than address.');
return parseInt(hashCharacter, 16) > 7
? character.toUpperCase()
: character;
})
.join('')}`;
}

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

return getChecksumAddress(possibleChecksum) === possibleChecksum;
}

/**
* Add the `0x`-prefix to a hexadecimal string. If the string already has the
* prefix, it is returned as-is.
Expand Down
53 changes: 27 additions & 26 deletions 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 @@ -1099,19 +1100,19 @@ __metadata:
languageName: unknown
linkType: soft

"@noble/curves@npm:1.0.0, @noble/curves@npm:~1.0.0":
version: 1.0.0
resolution: "@noble/curves@npm:1.0.0"
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
dependencies:
"@noble/hashes": 1.3.0
checksum: 6bcef44d626c640dc8961819d68dd67dffb907e3b973b7c27efe0ecdd9a5c6ce62c7b9e3dfc930c66605dced7f1ec0514d191c09a2ce98d6d52b66e3315ffa79
"@noble/hashes": 1.3.1
checksum: 2658cdd3f84f71079b4e3516c47559d22cf4b55c23ac8ee9d2b1f8e5b72916d9689e59820e0f9d9cb4a46a8423af5b56dc6bb7782405c88be06a015180508db5
languageName: node
linkType: hard

"@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
"@noble/hashes@npm:1.3.1, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1":
version: 1.3.1
resolution: "@noble/hashes@npm:1.3.1"
checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1
languageName: node
linkType: hard

Expand Down Expand Up @@ -1211,24 +1212,24 @@ __metadata:
languageName: node
linkType: hard

"@scure/bip32@npm:1.3.0":
version: 1.3.0
resolution: "@scure/bip32@npm:1.3.0"
"@scure/bip32@npm:1.3.1":
version: 1.3.1
resolution: "@scure/bip32@npm:1.3.1"
dependencies:
"@noble/curves": ~1.0.0
"@noble/hashes": ~1.3.0
"@noble/curves": ~1.1.0
"@noble/hashes": ~1.3.1
"@scure/base": ~1.1.0
checksum: 6eae997f9bdf41fe848134898960ac48e645fa10e63d579be965ca331afd0b7c1b8ebac170770d237ab4099dafc35e5a82995384510025ccf2abe669f85e8918
checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2
languageName: node
linkType: hard

"@scure/bip39@npm:1.2.0":
version: 1.2.0
resolution: "@scure/bip39@npm:1.2.0"
"@scure/bip39@npm:1.2.1":
version: 1.2.1
resolution: "@scure/bip39@npm:1.2.1"
dependencies:
"@noble/hashes": ~1.3.0
"@scure/base": ~1.1.0
checksum: 980d761f53e63de04a9e4db840eb13bfb1bd1b664ecb04a71824c12c190f4972fd84146f3ed89b2a8e4c6bd2c17c15f8b592b7ac029e903323b0f9e2dae6916b
checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa
languageName: node
linkType: hard

Expand Down Expand Up @@ -3223,14 +3224,14 @@ __metadata:
linkType: hard

"ethereum-cryptography@npm:^2.0.0":
version: 2.0.0
resolution: "ethereum-cryptography@npm:2.0.0"
version: 2.1.0
resolution: "ethereum-cryptography@npm:2.1.0"
dependencies:
"@noble/curves": 1.0.0
"@noble/hashes": 1.3.0
"@scure/bip32": 1.3.0
"@scure/bip39": 1.2.0
checksum: 958f8aab2d1b32aa759fb27a27877b3647410e8bb9aca7d65d1d477db4864cf7fc46b918eb52a1e246c25e98ee0a35a632c88b496aeaefa13469ee767a76c8db
"@noble/curves": 1.1.0
"@noble/hashes": 1.3.1
"@scure/bip32": 1.3.1
"@scure/bip39": 1.2.1
checksum: 47bd69103f0553e5c98e0645c295ca74e0da53a92b8d26237287f528521cd2aa13d5cd1e288c36e59ce885451199cef8e4de424a93c45bacf54a06bdd09946a4
languageName: node
linkType: hard

Expand Down

0 comments on commit f1024ca

Please sign in to comment.