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 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
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should do more validation here, such as checking if the provided address is 20 bytes long.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added an assertion to check the string we receive here

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