Skip to content

Commit fee24ec

Browse files
committed
chore: use faster address checksum functions
1 parent af5e7c6 commit fee24ec

File tree

4 files changed

+175
-12
lines changed

4 files changed

+175
-12
lines changed

packages/controller-utils/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
4848
},
4949
"dependencies": {
50-
"@ethereumjs/util": "^9.1.0",
5150
"@metamask/eth-query": "^4.0.0",
5251
"@metamask/ethjs-unit": "^0.3.0",
5352
"@metamask/utils": "^11.2.0",
@@ -57,12 +56,14 @@
5756
"bn.js": "^5.2.1",
5857
"cockatiel": "^3.1.2",
5958
"eth-ens-namehash": "^2.0.8",
60-
"fast-deep-equal": "^3.1.3"
59+
"fast-deep-equal": "^3.1.3",
60+
"lodash.memoize": "^4.1.2"
6161
},
6262
"devDependencies": {
6363
"@babel/runtime": "^7.23.9",
6464
"@metamask/auto-changelog": "^3.4.4",
6565
"@types/jest": "^27.4.1",
66+
"@types/lodash.memoize": "^4.1.9",
6667
"deepmerge": "^4.2.2",
6768
"jest": "^27.5.1",
6869
"jest-environment-jsdom": "^27.5.1",

packages/controller-utils/src/util.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import BigNumber from 'bignumber.js';
33
import BN from 'bn.js';
44
import nock from 'nock';
55

6-
import { FakeProvider } from '../../../tests/fake-provider';
76
import { MAX_SAFE_CHAIN_ID } from './constants';
87
import * as util from './util';
8+
import { FakeProvider } from '../../../tests/fake-provider';
99

1010
const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
1111
const SOME_API = 'https://someapi.com';
@@ -320,6 +320,52 @@ describe('util', () => {
320320
it('should return the input untouched if it is null', () => {
321321
expect(util.toChecksumHexAddress(null)).toBeNull();
322322
});
323+
324+
it('should memoize results for same input', () => {
325+
const testAddress = '4e1ff7229bddaf0a73df183a88d9c3a04cc975e0';
326+
327+
// Call the function multiple times with the same input
328+
const result1 = util.toChecksumHexAddress(testAddress);
329+
const result2 = util.toChecksumHexAddress(testAddress);
330+
const result3 = util.toChecksumHexAddress(testAddress);
331+
332+
// All results should be identical
333+
expect(result1).toBe('0x4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0');
334+
expect(result2).toBe(result1);
335+
expect(result3).toBe(result1);
336+
});
337+
338+
it('should return different results for different inputs but still memoize each', () => {
339+
const testAddress1 = '4e1ff7229bddaf0a73df183a88d9c3a04cc975e0';
340+
const testAddress2 = '742d35cc6ba4c0a2b7e8b4c0b1b0c2b2b2b2b2b2';
341+
342+
// Call with first address multiple times
343+
const result1a = util.toChecksumHexAddress(testAddress1);
344+
const result1b = util.toChecksumHexAddress(testAddress1);
345+
346+
// Call with second address multiple times
347+
const result2a = util.toChecksumHexAddress(testAddress2);
348+
const result2b = util.toChecksumHexAddress(testAddress2);
349+
350+
// Results for same address should be identical
351+
expect(result1b).toBe(result1a);
352+
expect(result2b).toBe(result2a);
353+
354+
// Results for different addresses should be different
355+
expect(result1a).not.toBe(result2a);
356+
});
357+
358+
it('should memoize based on complete argument signature', () => {
359+
const testAddress = '4e1ff7229bddaf0a73df183a88d9c3a04cc975e0';
360+
361+
// Call with string argument
362+
const result1 = util.toChecksumHexAddress(testAddress);
363+
const result2 = util.toChecksumHexAddress(testAddress);
364+
365+
// Both should be memoized and return the same result
366+
expect(result2).toBe(result1);
367+
expect(result1).toBe('0x4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0');
368+
});
323369
});
324370

325371
describe('isValidHexAddress', () => {
@@ -336,6 +382,83 @@ describe('util', () => {
336382
false,
337383
);
338384
});
385+
386+
it('should memoize results for same input', () => {
387+
const validAddress = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
388+
389+
// Call the function multiple times with the same input
390+
const result1 = util.isValidHexAddress(validAddress);
391+
const result2 = util.isValidHexAddress(validAddress);
392+
const result3 = util.isValidHexAddress(validAddress);
393+
394+
// All results should be identical
395+
expect(result1).toBe(true);
396+
expect(result2).toBe(result1);
397+
expect(result3).toBe(result1);
398+
});
399+
400+
it('should memoize results for same input with options', () => {
401+
const validAddress = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
402+
const options = { allowNonPrefixed: true };
403+
404+
// Call the function multiple times with the same input and options
405+
const result1 = util.isValidHexAddress(validAddress, options);
406+
const result2 = util.isValidHexAddress(validAddress, options);
407+
const result3 = util.isValidHexAddress(validAddress, options);
408+
409+
// All results should be identical
410+
expect(result1).toBe(true);
411+
expect(result2).toBe(result1);
412+
expect(result3).toBe(result1);
413+
});
414+
415+
it('should return different results for different option combinations', () => {
416+
const addressWithoutPrefix = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
417+
418+
// Call with different options
419+
const result1 = util.isValidHexAddress(addressWithoutPrefix, {
420+
allowNonPrefixed: true,
421+
});
422+
const result2 = util.isValidHexAddress(addressWithoutPrefix, {
423+
allowNonPrefixed: false,
424+
});
425+
426+
// Should return different results for different options
427+
expect(result1).toBe(true);
428+
expect(result2).toBe(false);
429+
430+
// But calling again with same options should return memoized results
431+
const result1Again = util.isValidHexAddress(addressWithoutPrefix, {
432+
allowNonPrefixed: true,
433+
});
434+
const result2Again = util.isValidHexAddress(addressWithoutPrefix, {
435+
allowNonPrefixed: false,
436+
});
437+
438+
expect(result1Again).toBe(result1);
439+
expect(result2Again).toBe(result2);
440+
});
441+
442+
it('should handle memoization with different address inputs', () => {
443+
const validAddress = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
444+
const invalidAddress = '0x00';
445+
446+
// Call with valid address multiple times
447+
const validResult1 = util.isValidHexAddress(validAddress);
448+
const validResult2 = util.isValidHexAddress(validAddress);
449+
450+
// Call with invalid address multiple times
451+
const invalidResult1 = util.isValidHexAddress(invalidAddress);
452+
const invalidResult2 = util.isValidHexAddress(invalidAddress);
453+
454+
// Results for same address should be identical
455+
expect(validResult2).toBe(validResult1);
456+
expect(invalidResult2).toBe(invalidResult1);
457+
458+
// Results should be correct
459+
expect(validResult1).toBe(true);
460+
expect(invalidResult1).toBe(false);
461+
});
339462
});
340463

341464
it('messageHexToString', () => {

packages/controller-utils/src/util.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isValidAddress, toChecksumAddress } from '@ethereumjs/util';
21
import type EthQuery from '@metamask/eth-query';
32
import { fromWei, toWei } from '@metamask/ethjs-unit';
43
import type { Hex, Json } from '@metamask/utils';
@@ -7,11 +6,15 @@ import {
76
add0x,
87
isHexString,
98
remove0x,
9+
getChecksumAddress,
10+
// TODO: we need to bump package version to get this
11+
isHexChecksumAddress,
1012
} from '@metamask/utils';
1113
import type { BigNumber } from 'bignumber.js';
1214
import BN from 'bn.js';
1315
import ensNamehash from 'eth-ens-namehash';
1416
import deepEqual from 'fast-deep-equal';
17+
import memoize from 'lodash.memoize';
1518

1619
import { MAX_SAFE_CHAIN_ID } from './constants';
1720

@@ -284,7 +287,7 @@ export async function safelyExecuteWithTimeout<Result>(
284287
* @param address - The address to convert.
285288
* @returns The address in 0x-prefixed hexadecimal checksummed form if it is valid.
286289
*/
287-
export function toChecksumHexAddress(address: string): string;
290+
function toChecksumHexAddressUnmemoized(address: string): string;
288291

289292
/**
290293
* Convert an address to a checksummed hexadecimal address.
@@ -299,11 +302,11 @@ export function toChecksumHexAddress(address: string): string;
299302
*/
300303
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
301304
// eslint-disable-next-line @typescript-eslint/naming-convention
302-
export function toChecksumHexAddress<T>(address: T): T;
305+
function toChecksumHexAddressUnmemoized<T>(address: T): T;
303306

304307
// Tools only see JSDocs for overloads and ignore them for the implementation.
305308
// eslint-disable-next-line jsdoc/require-jsdoc
306-
export function toChecksumHexAddress(address: unknown) {
309+
function toChecksumHexAddressUnmemoized(address: unknown) {
307310
if (typeof address !== 'string') {
308311
// Mimic behavior of `addHexPrefix` from `ethereumjs-util` (which this
309312
// function was previously using) for backward compatibility.
@@ -320,9 +323,20 @@ export function toChecksumHexAddress(address: unknown) {
320323
return hexPrefixed;
321324
}
322325

323-
return toChecksumAddress(hexPrefixed);
326+
return getChecksumAddress(hexPrefixed);
324327
}
325328

329+
/**
330+
* Convert an address to a checksummed hexadecimal address.
331+
*
332+
* @param address - The address to convert.
333+
* @returns The address in 0x-prefixed hexadecimal checksummed form if it is valid.
334+
*/
335+
export const toChecksumHexAddress: {
336+
(address: string): string;
337+
<T>(address: T): T;
338+
} = memoize(toChecksumHexAddressUnmemoized);
339+
326340
/**
327341
* Validates that the input is a hex address. This utility method is a thin
328342
* wrapper around @metamask/utils.isValidHexAddress, with the exception that it
@@ -334,7 +348,7 @@ export function toChecksumHexAddress(address: unknown) {
334348
* @param options.allowNonPrefixed - If true will allow addresses without `0x` prefix.`
335349
* @returns Whether or not the input is a valid hex address.
336350
*/
337-
export function isValidHexAddress(
351+
function isValidHexAddressUnmemoized(
338352
possibleAddress: string,
339353
{ allowNonPrefixed = true } = {},
340354
): boolean {
@@ -345,9 +359,17 @@ export function isValidHexAddress(
345359
return false;
346360
}
347361

348-
return isValidAddress(addressToCheck);
362+
return isHexChecksumAddress(addressToCheck);
349363
}
350364

365+
export const isValidHexAddress: (
366+
address: string,
367+
options?: { allowNonPrefixed?: boolean },
368+
) => boolean = memoize(
369+
isValidHexAddressUnmemoized,
370+
(address, options) => `${address}-${options?.allowNonPrefixed}`,
371+
);
372+
351373
/**
352374
* Returns whether the given code corresponds to a smart contract.
353375
*

yarn.lock

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2917,14 +2917,14 @@ __metadata:
29172917
resolution: "@metamask/controller-utils@workspace:packages/controller-utils"
29182918
dependencies:
29192919
"@babel/runtime": "npm:^7.23.9"
2920-
"@ethereumjs/util": "npm:^9.1.0"
29212920
"@metamask/auto-changelog": "npm:^3.4.4"
29222921
"@metamask/eth-query": "npm:^4.0.0"
29232922
"@metamask/ethjs-unit": "npm:^0.3.0"
29242923
"@metamask/utils": "npm:^11.2.0"
29252924
"@spruceid/siwe-parser": "npm:2.1.0"
29262925
"@types/bn.js": "npm:^5.1.5"
29272926
"@types/jest": "npm:^27.4.1"
2927+
"@types/lodash.memoize": "npm:^4.1.9"
29282928
bignumber.js: "npm:^9.1.2"
29292929
bn.js: "npm:^5.2.1"
29302930
cockatiel: "npm:^3.1.2"
@@ -2933,6 +2933,7 @@ __metadata:
29332933
fast-deep-equal: "npm:^3.1.3"
29342934
jest: "npm:^27.5.1"
29352935
jest-environment-jsdom: "npm:^27.5.1"
2936+
lodash.memoize: "npm:^4.1.2"
29362937
nock: "npm:^13.3.1"
29372938
sinon: "npm:^9.2.4"
29382939
ts-jest: "npm:^27.1.4"
@@ -5593,6 +5594,22 @@ __metadata:
55935594
languageName: node
55945595
linkType: hard
55955596

5597+
"@types/lodash.memoize@npm:^4.1.9":
5598+
version: 4.1.9
5599+
resolution: "@types/lodash.memoize@npm:4.1.9"
5600+
dependencies:
5601+
"@types/lodash": "npm:*"
5602+
checksum: 10/d11efe604911aabbf9c49eb02e944de856619d6e0ab348d83be3ff07de245ee605ea71b1f3ee24b5c134286d02625119edf3ac2c0e6aa4732f699b1f4aa55240
5603+
languageName: node
5604+
linkType: hard
5605+
5606+
"@types/lodash@npm:*":
5607+
version: 4.17.19
5608+
resolution: "@types/lodash@npm:4.17.19"
5609+
checksum: 10/555e53da60dd9904ef4b6f4e663abb965cf45582079f2ea231a18b0b48df4808b60f36c978548fcf5e307bf12ccd563bd4551963039028ea120b1695c7146f7c
5610+
languageName: node
5611+
linkType: hard
5612+
55965613
"@types/lodash@npm:^4.14.191":
55975614
version: 4.17.7
55985615
resolution: "@types/lodash@npm:4.17.7"
@@ -11556,7 +11573,7 @@ __metadata:
1155611573
languageName: node
1155711574
linkType: hard
1155811575

11559-
"lodash.memoize@npm:4.x":
11576+
"lodash.memoize@npm:4.x, lodash.memoize@npm:^4.1.2":
1156011577
version: 4.1.2
1156111578
resolution: "lodash.memoize@npm:4.1.2"
1156211579
checksum: 10/192b2168f310c86f303580b53acf81ab029761b9bd9caa9506a019ffea5f3363ea98d7e39e7e11e6b9917066c9d36a09a11f6fe16f812326390d8f3a54a1a6da

0 commit comments

Comments
 (0)