Skip to content

Commit 2f1404a

Browse files
committed
feat(metrics): add eth-chainlist domain check to isPublicEndpointUrl
1 parent e63d921 commit 2f1404a

File tree

3 files changed

+333
-52
lines changed

3 files changed

+333
-52
lines changed

app/scripts/lib/util.ts

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
2626
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
2727
import { getMethodDataAsync } from '../../../shared/lib/four-byte';
28-
import { getSafeChainsListFromCacheOnly } from '../../../shared/lib/network-utils';
28+
import {
29+
initializeChainlistDomains,
30+
isChainlistDomain,
31+
} from '../../../shared/lib/network-utils';
2932

3033
/**
3134
* @see {@link getEnvironmentType}
@@ -474,53 +477,12 @@ export function getConversionRatesForNativeAsset({
474477
return conversionRateResult;
475478
}
476479

477-
// Cache for known domains
478-
let knownDomainsSet: Set<string> | null = null;
479-
let initPromise: Promise<void> | null = null;
480-
481-
/**
482-
* Initialize the set of known domains from the chains list
483-
*/
484-
export async function initializeRpcProviderDomains(): Promise<void> {
485-
if (initPromise) {
486-
return initPromise;
487-
}
488-
489-
initPromise = (async () => {
490-
try {
491-
const chainsList = await getSafeChainsListFromCacheOnly();
492-
knownDomainsSet = new Set<string>();
493-
494-
for (const chain of chainsList) {
495-
if (chain.rpc && Array.isArray(chain.rpc)) {
496-
for (const rpcUrl of chain.rpc) {
497-
try {
498-
const url = new URL(rpcUrl);
499-
knownDomainsSet.add(url.hostname);
500-
} catch (e) {
501-
// Skip invalid URLs
502-
continue;
503-
}
504-
}
505-
}
506-
}
507-
} catch (error) {
508-
console.error('Error initializing known domains:', error);
509-
knownDomainsSet = new Set<string>();
510-
}
511-
})();
512-
513-
return initPromise;
514-
}
515-
516-
/**
517-
* Check if a domain is in the known domains list
518-
*
519-
* @param domain - The domain to check
520-
*/
521-
export function isKnownDomain(domain: string): boolean {
522-
return knownDomainsSet?.has(domain?.toLowerCase()) ?? false;
523-
}
480+
// Re-export chainlist domain functions for backward compatibility
481+
// These were moved to shared/lib/network-utils.ts
482+
export {
483+
initializeChainlistDomains as initializeRpcProviderDomains,
484+
isChainlistDomain as isKnownDomain,
485+
};
524486

525487
/**
526488
* Extracts the domain from an RPC endpoint URL with privacy considerations
@@ -556,12 +518,12 @@ export function extractRpcDomain(
556518
}
557519
}
558520

559-
// Use the provided test domains if available, otherwise use isKnownDomain
521+
// Use the provided test domains if available, otherwise use isChainlistDomain
560522
if (knownDomainsForTesting) {
561523
if (knownDomainsForTesting.has(url.hostname.toLowerCase())) {
562524
return url.hostname.toLowerCase();
563525
}
564-
} else if (isKnownDomain(url.hostname)) {
526+
} else if (isChainlistDomain(url.hostname)) {
565527
return url.hostname.toLowerCase();
566528
}
567529

shared/lib/network-utils.test.ts

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
getIsMetaMaskInfuraEndpointUrl,
99
getIsQuicknodeEndpointUrl,
1010
isPublicEndpointUrl,
11+
initializeChainlistDomains,
12+
isChainlistDomain,
13+
isChainlistEndpointUrl,
14+
resetChainlistDomainsCache,
1115
} from './network-utils';
16+
import * as storageHelpers from './storage-helpers';
1217

1318
jest.mock('../constants/network', () => ({
1419
FEATURED_RPCS: [
@@ -193,12 +198,231 @@ describe('isPublicEndpointUrl', () => {
193198
).toBe(true);
194199
});
195200

196-
it('returns false for unknown URLs', () => {
201+
it('returns false for unknown URLs when chainlist is not initialized', () => {
202+
resetChainlistDomainsCache();
197203
expect(
198204
isPublicEndpointUrl(
199205
'https://unknown.example.com',
200206
MOCK_METAMASK_INFURA_PROJECT_ID,
201207
),
202208
).toBe(false);
203209
});
210+
211+
it('returns true for chainlist domains after initialization', async () => {
212+
resetChainlistDomainsCache();
213+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
214+
cachedResponse: [
215+
{
216+
name: 'Test Chain',
217+
chainId: 1,
218+
rpc: ['https://chainlist-rpc.example.com/rpc'],
219+
},
220+
],
221+
});
222+
223+
await initializeChainlistDomains();
224+
225+
expect(
226+
isPublicEndpointUrl(
227+
'https://chainlist-rpc.example.com/v1/abc123',
228+
MOCK_METAMASK_INFURA_PROJECT_ID,
229+
),
230+
).toBe(true);
231+
});
232+
});
233+
234+
describe('initializeChainlistDomains', () => {
235+
beforeEach(() => {
236+
resetChainlistDomainsCache();
237+
jest.clearAllMocks();
238+
});
239+
240+
it('initializes domains from cached chains list', async () => {
241+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
242+
cachedResponse: [
243+
{
244+
name: 'Ethereum Mainnet',
245+
chainId: 1,
246+
rpc: [
247+
'https://mainnet.infura.io/v3/key',
248+
'https://eth-mainnet.alchemyapi.io/v2/key',
249+
],
250+
},
251+
{
252+
name: 'Polygon',
253+
chainId: 137,
254+
rpc: ['https://polygon-rpc.com'],
255+
},
256+
],
257+
});
258+
259+
await initializeChainlistDomains();
260+
261+
expect(isChainlistDomain('mainnet.infura.io')).toBe(true);
262+
expect(isChainlistDomain('eth-mainnet.alchemyapi.io')).toBe(true);
263+
expect(isChainlistDomain('polygon-rpc.com')).toBe(true);
264+
expect(isChainlistDomain('unknown.com')).toBe(false);
265+
});
266+
267+
it('handles empty chains list', async () => {
268+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
269+
cachedResponse: [],
270+
});
271+
272+
await initializeChainlistDomains();
273+
274+
expect(isChainlistDomain('any-domain.com')).toBe(false);
275+
});
276+
277+
it('handles missing cache', async () => {
278+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue(undefined);
279+
280+
await initializeChainlistDomains();
281+
282+
expect(isChainlistDomain('any-domain.com')).toBe(false);
283+
});
284+
285+
it('handles chains without rpc field', async () => {
286+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
287+
cachedResponse: [
288+
{
289+
name: 'Chain Without RPC',
290+
chainId: 999,
291+
},
292+
],
293+
});
294+
295+
await initializeChainlistDomains();
296+
297+
expect(isChainlistDomain('any-domain.com')).toBe(false);
298+
});
299+
300+
it('skips invalid URLs in rpc list', async () => {
301+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
302+
cachedResponse: [
303+
{
304+
name: 'Chain With Invalid RPC',
305+
chainId: 999,
306+
rpc: ['not-a-valid-url', 'https://valid-rpc.com'],
307+
},
308+
],
309+
});
310+
311+
await initializeChainlistDomains();
312+
313+
expect(isChainlistDomain('valid-rpc.com')).toBe(true);
314+
});
315+
316+
it('only fetches from storage once on subsequent calls', async () => {
317+
const getStorageItemSpy = jest
318+
.spyOn(storageHelpers, 'getStorageItem')
319+
.mockResolvedValue({
320+
cachedResponse: [],
321+
});
322+
323+
await initializeChainlistDomains();
324+
await initializeChainlistDomains();
325+
await initializeChainlistDomains();
326+
327+
// Storage should only be accessed once despite multiple calls
328+
expect(getStorageItemSpy).toHaveBeenCalledTimes(1);
329+
});
330+
});
331+
332+
describe('isChainlistDomain', () => {
333+
beforeEach(() => {
334+
resetChainlistDomainsCache();
335+
});
336+
337+
it('returns false when not initialized', () => {
338+
expect(isChainlistDomain('any-domain.com')).toBe(false);
339+
});
340+
341+
it('returns false for empty domain', async () => {
342+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
343+
cachedResponse: [{ name: 'Test', chainId: 1, rpc: ['https://test.com'] }],
344+
});
345+
await initializeChainlistDomains();
346+
347+
expect(isChainlistDomain('')).toBe(false);
348+
});
349+
350+
it('is case insensitive', async () => {
351+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
352+
cachedResponse: [
353+
{ name: 'Test', chainId: 1, rpc: ['https://Test-RPC.COM/path'] },
354+
],
355+
});
356+
await initializeChainlistDomains();
357+
358+
expect(isChainlistDomain('test-rpc.com')).toBe(true);
359+
expect(isChainlistDomain('TEST-RPC.COM')).toBe(true);
360+
expect(isChainlistDomain('Test-RPC.com')).toBe(true);
361+
});
362+
});
363+
364+
describe('isChainlistEndpointUrl', () => {
365+
beforeEach(() => {
366+
resetChainlistDomainsCache();
367+
});
368+
369+
it('returns false when not initialized', () => {
370+
expect(isChainlistEndpointUrl('https://any-domain.com/rpc')).toBe(false);
371+
});
372+
373+
it('returns true for URLs with chainlist domains', async () => {
374+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
375+
cachedResponse: [
376+
{ name: 'Test', chainId: 1, rpc: ['https://chainlist-domain.com'] },
377+
],
378+
});
379+
await initializeChainlistDomains();
380+
381+
expect(
382+
isChainlistEndpointUrl('https://chainlist-domain.com/v1/abc123'),
383+
).toBe(true);
384+
expect(isChainlistEndpointUrl('https://chainlist-domain.com')).toBe(true);
385+
});
386+
387+
it('returns false for URLs with unknown domains', async () => {
388+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
389+
cachedResponse: [
390+
{ name: 'Test', chainId: 1, rpc: ['https://known-domain.com'] },
391+
],
392+
});
393+
await initializeChainlistDomains();
394+
395+
expect(isChainlistEndpointUrl('https://unknown-domain.com/rpc')).toBe(
396+
false,
397+
);
398+
});
399+
400+
it('returns false for invalid URLs', () => {
401+
expect(isChainlistEndpointUrl('not-a-url')).toBe(false);
402+
expect(isChainlistEndpointUrl('')).toBe(false);
403+
});
404+
});
405+
406+
describe('resetChainlistDomainsCache', () => {
407+
it('clears the cache and allows reinitialization', async () => {
408+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
409+
cachedResponse: [
410+
{ name: 'First', chainId: 1, rpc: ['https://first-domain.com'] },
411+
],
412+
});
413+
await initializeChainlistDomains();
414+
expect(isChainlistDomain('first-domain.com')).toBe(true);
415+
416+
resetChainlistDomainsCache();
417+
expect(isChainlistDomain('first-domain.com')).toBe(false);
418+
419+
jest.spyOn(storageHelpers, 'getStorageItem').mockResolvedValue({
420+
cachedResponse: [
421+
{ name: 'Second', chainId: 2, rpc: ['https://second-domain.com'] },
422+
],
423+
});
424+
await initializeChainlistDomains();
425+
expect(isChainlistDomain('second-domain.com')).toBe(true);
426+
expect(isChainlistDomain('first-domain.com')).toBe(false);
427+
});
204428
});

0 commit comments

Comments
 (0)