Skip to content

Commit f1024ca

Browse files
mikespositolegobeatMrtenzGudahtt
authored
Add address related utils (#112)
* 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]>
1 parent 47b757d commit f1024ca

File tree

4 files changed

+180
-26
lines changed

4 files changed

+180
-26
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
},
3939
"dependencies": {
4040
"@ethereumjs/tx": "^4.1.2",
41+
"@noble/hashes": "^1.3.1",
4142
"@types/debug": "^4.1.7",
4243
"debug": "^4.3.4",
4344
"semver": "^7.3.8",

src/hex.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import {
2+
Hex,
23
add0x,
34
assertIsHexString,
45
assertIsStrictHexString,
6+
isValidChecksumAddress,
57
isHexString,
68
isStrictHexString,
9+
isValidHexAddress,
710
remove0x,
11+
getChecksumAddress,
812
} from './hex';
913

1014
describe('isHexString', () => {
@@ -151,6 +155,92 @@ describe('assertIsStrictHexString', () => {
151155
});
152156
});
153157

158+
describe('isValidHexAddress', () => {
159+
it.each([
160+
'0x0000000000000000000000000000000000000000' as Hex,
161+
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
162+
])('returns true for a valid prefixed hex address', (hexString) => {
163+
expect(isValidHexAddress(hexString)).toBe(true);
164+
});
165+
166+
it.each([
167+
'0000000000000000000000000000000000000000',
168+
'd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
169+
])('returns false for a valid non-prefixed hex address', (hexString) => {
170+
// @ts-expect-error - testing invalid input
171+
expect(isValidHexAddress(hexString)).toBe(false);
172+
});
173+
174+
it.each([
175+
'12345g',
176+
'1234567890abcdefg',
177+
'1234567890abcdefG',
178+
'1234567890abcdefABCDEFg',
179+
'1234567890abcdefABCDEF1234567890abcdefABCDEFg',
180+
'0x',
181+
'0x0',
182+
'0x12345g',
183+
'0x1234567890abcdefg',
184+
'0x1234567890abcdefG',
185+
'0x1234567890abcdefABCDEFg',
186+
'0x1234567890abcdefABCDEF1234567890abcdefABCDEFg',
187+
'0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045',
188+
'0xCF5609B003B2776699EEA1233F7C82D5695CC9AA',
189+
'0Xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
190+
])('returns false for an invalid hex address', (hexString) => {
191+
// @ts-expect-error - testing invalid input
192+
expect(isValidHexAddress(hexString)).toBe(false);
193+
});
194+
});
195+
196+
describe('getChecksumAddress', () => {
197+
it('returns the checksum address for a valid hex address', () => {
198+
expect(
199+
getChecksumAddress('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed'),
200+
).toBe('0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed');
201+
202+
expect(
203+
getChecksumAddress('0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359'),
204+
).toBe('0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359');
205+
206+
expect(
207+
getChecksumAddress('0x52908400098527886e0f7030069857d2e4169ee7'),
208+
).toBe('0x52908400098527886E0F7030069857D2E4169EE7');
209+
210+
expect(
211+
getChecksumAddress('0xde709f2102306220921060314715629080e2fb77'),
212+
).toBe('0xde709f2102306220921060314715629080e2fb77');
213+
214+
expect(
215+
getChecksumAddress('0x0000000000000000000000000000000000000000'),
216+
).toBe('0x0000000000000000000000000000000000000000');
217+
});
218+
219+
it('throws for an invalid hex address', () => {
220+
expect(() => getChecksumAddress('0x')).toThrow('Invalid hex address.');
221+
});
222+
});
223+
224+
describe('isValidChecksumAddress', () => {
225+
it.each([
226+
'0x0000000000000000000000000000000000000000' as Hex,
227+
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
228+
'0xCf5609B003B2776699eEA1233F7C82D5695cC9AA' as Hex,
229+
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Hex,
230+
'0x8617E340B3D01FA5F11F306F4090FD50E238070D' as Hex,
231+
])('returns true for a valid checksum address', (hexString) => {
232+
expect(isValidChecksumAddress(hexString)).toBe(true);
233+
});
234+
235+
it.each([
236+
'0xz' as Hex,
237+
'0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045' as Hex,
238+
'0xCF5609B003B2776699EEA1233F7C82D5695CC9AA' as Hex,
239+
])('returns false for an invalid checksum address', (hexString) => {
240+
expect(isValidChecksumAddress(hexString)).toBe(false);
241+
});
242+
});
243+
154244
describe('add0x', () => {
155245
it('adds a 0x-prefix to a string', () => {
156246
expect(add0x('12345')).toBe('0x12345');

src/hex.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
12
import { is, pattern, string, Struct } from 'superstruct';
23

34
import { assert } from './assert';
5+
import { bytesToHex } from './bytes';
46

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

@@ -9,6 +11,14 @@ export const StrictHexStruct = pattern(string(), /^0x[0-9a-f]+$/iu) as Struct<
911
Hex,
1012
null
1113
>;
14+
export const HexAddressStruct = pattern(
15+
string(),
16+
/^0x[0-9a-f]{40}$/u,
17+
) as Struct<Hex, null>;
18+
export const HexChecksumAddressStruct = pattern(
19+
string(),
20+
/^0x[0-9a-fA-F]{40}$/u,
21+
) as Struct<Hex, null>;
1222

1323
/**
1424
* Check if a string is a valid hex string.
@@ -55,6 +65,58 @@ export function assertIsStrictHexString(value: unknown): asserts value is Hex {
5565
);
5666
}
5767

68+
/**
69+
* Validate that the passed prefixed hex string is an all-lowercase
70+
* hex address, or a valid mixed-case checksum address.
71+
*
72+
* @param possibleAddress - Input parameter to check against.
73+
* @returns Whether or not the input is a valid hex address.
74+
*/
75+
export function isValidHexAddress(possibleAddress: Hex) {
76+
return (
77+
is(possibleAddress, HexAddressStruct) ||
78+
isValidChecksumAddress(possibleAddress)
79+
);
80+
}
81+
82+
/**
83+
* Encode a passed hex string as an ERC-55 mixed-case checksum address.
84+
*
85+
* @param address - The hex address to encode.
86+
* @returns The address encoded according to ERC-55.
87+
* @see https://eips.ethereum.org/EIPS/eip-55
88+
*/
89+
export function getChecksumAddress(address: Hex) {
90+
assert(is(address, HexChecksumAddressStruct), 'Invalid hex address.');
91+
const unPrefixed = remove0x(address.toLowerCase());
92+
const unPrefixedHash = remove0x(bytesToHex(keccak256(unPrefixed)));
93+
return `0x${unPrefixed
94+
.split('')
95+
.map((character, nibbleIndex) => {
96+
const hashCharacter = unPrefixedHash[nibbleIndex];
97+
assert(is(hashCharacter, string()), 'Hash shorter than address.');
98+
return parseInt(hashCharacter, 16) > 7
99+
? character.toUpperCase()
100+
: character;
101+
})
102+
.join('')}`;
103+
}
104+
105+
/**
106+
* Validate that the passed hex string is a valid ERC-55 mixed-case
107+
* checksum address.
108+
*
109+
* @param possibleChecksum - The hex address to check.
110+
* @returns True if the address is a checksum address.
111+
*/
112+
export function isValidChecksumAddress(possibleChecksum: Hex) {
113+
if (!is(possibleChecksum, HexChecksumAddressStruct)) {
114+
return false;
115+
}
116+
117+
return getChecksumAddress(possibleChecksum) === possibleChecksum;
118+
}
119+
58120
/**
59121
* Add the `0x`-prefix to a hexadecimal string. If the string already has the
60122
* prefix, it is returned as-is.

yarn.lock

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@ __metadata:
10691069
"@metamask/eslint-config-jest": ^11.0.0
10701070
"@metamask/eslint-config-nodejs": ^11.0.1
10711071
"@metamask/eslint-config-typescript": ^11.0.0
1072+
"@noble/hashes": ^1.3.1
10721073
"@types/debug": ^4.1.7
10731074
"@types/jest": ^28.1.7
10741075
"@types/node": ^17.0.23
@@ -1099,19 +1100,19 @@ __metadata:
10991100
languageName: unknown
11001101
linkType: soft
11011102

1102-
"@noble/curves@npm:1.0.0, @noble/curves@npm:~1.0.0":
1103-
version: 1.0.0
1104-
resolution: "@noble/curves@npm:1.0.0"
1103+
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
1104+
version: 1.1.0
1105+
resolution: "@noble/curves@npm:1.1.0"
11051106
dependencies:
1106-
"@noble/hashes": 1.3.0
1107-
checksum: 6bcef44d626c640dc8961819d68dd67dffb907e3b973b7c27efe0ecdd9a5c6ce62c7b9e3dfc930c66605dced7f1ec0514d191c09a2ce98d6d52b66e3315ffa79
1107+
"@noble/hashes": 1.3.1
1108+
checksum: 2658cdd3f84f71079b4e3516c47559d22cf4b55c23ac8ee9d2b1f8e5b72916d9689e59820e0f9d9cb4a46a8423af5b56dc6bb7782405c88be06a015180508db5
11081109
languageName: node
11091110
linkType: hard
11101111

1111-
"@noble/hashes@npm:1.3.0, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:~1.3.0":
1112-
version: 1.3.0
1113-
resolution: "@noble/hashes@npm:1.3.0"
1114-
checksum: d7ddb6d7c60f1ce1f87facbbef5b724cdea536fc9e7f59ae96e0fc9de96c8f1a2ae2bdedbce10f7dcc621338dfef8533daa73c873f2b5c87fa1a4e05a95c2e2e
1112+
"@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":
1113+
version: 1.3.1
1114+
resolution: "@noble/hashes@npm:1.3.1"
1115+
checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1
11151116
languageName: node
11161117
linkType: hard
11171118

@@ -1211,24 +1212,24 @@ __metadata:
12111212
languageName: node
12121213
linkType: hard
12131214

1214-
"@scure/bip32@npm:1.3.0":
1215-
version: 1.3.0
1216-
resolution: "@scure/bip32@npm:1.3.0"
1215+
"@scure/bip32@npm:1.3.1":
1216+
version: 1.3.1
1217+
resolution: "@scure/bip32@npm:1.3.1"
12171218
dependencies:
1218-
"@noble/curves": ~1.0.0
1219-
"@noble/hashes": ~1.3.0
1219+
"@noble/curves": ~1.1.0
1220+
"@noble/hashes": ~1.3.1
12201221
"@scure/base": ~1.1.0
1221-
checksum: 6eae997f9bdf41fe848134898960ac48e645fa10e63d579be965ca331afd0b7c1b8ebac170770d237ab4099dafc35e5a82995384510025ccf2abe669f85e8918
1222+
checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2
12221223
languageName: node
12231224
linkType: hard
12241225

1225-
"@scure/bip39@npm:1.2.0":
1226-
version: 1.2.0
1227-
resolution: "@scure/bip39@npm:1.2.0"
1226+
"@scure/bip39@npm:1.2.1":
1227+
version: 1.2.1
1228+
resolution: "@scure/bip39@npm:1.2.1"
12281229
dependencies:
12291230
"@noble/hashes": ~1.3.0
12301231
"@scure/base": ~1.1.0
1231-
checksum: 980d761f53e63de04a9e4db840eb13bfb1bd1b664ecb04a71824c12c190f4972fd84146f3ed89b2a8e4c6bd2c17c15f8b592b7ac029e903323b0f9e2dae6916b
1232+
checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa
12321233
languageName: node
12331234
linkType: hard
12341235

@@ -3223,14 +3224,14 @@ __metadata:
32233224
linkType: hard
32243225

32253226
"ethereum-cryptography@npm:^2.0.0":
3226-
version: 2.0.0
3227-
resolution: "ethereum-cryptography@npm:2.0.0"
3227+
version: 2.1.0
3228+
resolution: "ethereum-cryptography@npm:2.1.0"
32283229
dependencies:
3229-
"@noble/curves": 1.0.0
3230-
"@noble/hashes": 1.3.0
3231-
"@scure/bip32": 1.3.0
3232-
"@scure/bip39": 1.2.0
3233-
checksum: 958f8aab2d1b32aa759fb27a27877b3647410e8bb9aca7d65d1d477db4864cf7fc46b918eb52a1e246c25e98ee0a35a632c88b496aeaefa13469ee767a76c8db
3230+
"@noble/curves": 1.1.0
3231+
"@noble/hashes": 1.3.1
3232+
"@scure/bip32": 1.3.1
3233+
"@scure/bip39": 1.2.1
3234+
checksum: 47bd69103f0553e5c98e0645c295ca74e0da53a92b8d26237287f528521cd2aa13d5cd1e288c36e59ce885451199cef8e4de424a93c45bacf54a06bdd09946a4
32343235
languageName: node
32353236
linkType: hard
32363237

0 commit comments

Comments
 (0)