Skip to content

Commit f774423

Browse files
feat: add max size bounds for networks to add
this prevents users from adding thousands of networks, which could cause issues for the application
1 parent 75fce4a commit f774423

File tree

3 files changed

+161
-8
lines changed

3 files changed

+161
-8
lines changed

packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ type ControllerConfig = {
175175
};
176176

177177
networkSyncing?: {
178+
maxNumberOfNetworksToAdd?: number;
178179
/**
179180
* Callback that fires when network sync adds a network
180181
* This is used for analytics.
@@ -1176,6 +1177,7 @@ export default class UserStorageController extends BaseController<
11761177
await performMainNetworkSync({
11771178
messenger: this.messagingSystem,
11781179
getStorageConfig: () => this.#getStorageOptions(),
1180+
maxNetworksToAdd: this.#config?.networkSyncing?.maxNumberOfNetworksToAdd,
11791181
onNetworkAdded: (cId) =>
11801182
this.#config?.networkSyncing?.onNetworkAdded?.(profileId, cId),
11811183
onNetworkUpdated: (cId) =>

packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
import {
1515
performMainNetworkSync,
1616
startNetworkSyncing,
17+
calculateAvailableSpaceToAdd,
18+
getBoundedNetworksToAdd,
1719
} from './controller-integration';
1820
import * as ControllerIntegrationModule from './controller-integration';
1921
import * as ServicesModule from './services';
@@ -40,6 +42,69 @@ const storageOpts: UserStorageBaseOptions = {
4042
storageKey: MOCK_STORAGE_KEY,
4143
};
4244

45+
describe('network-syncing/controller-integration - calculateAvailableSpaceToAdd()', () => {
46+
it('returns available space to add', () => {
47+
expect(calculateAvailableSpaceToAdd(5, 10)).toBe(5);
48+
expect(calculateAvailableSpaceToAdd(9, 10)).toBe(1);
49+
});
50+
it('returns 0 if there is no available space to add', () => {
51+
expect(calculateAvailableSpaceToAdd(5, 5)).toBe(0);
52+
expect(calculateAvailableSpaceToAdd(10, 5)).toBe(0);
53+
});
54+
});
55+
56+
describe('network-syncing/controller-integration - getBoundedNetworksToAdd()', () => {
57+
it('returns networks to add if within bounds', () => {
58+
const originalNetworks = arrangeTestNetworks(['0x1', '0x2']);
59+
const networksToAdd = arrangeTestNetworks(['0x3', '0x4']);
60+
const result = getBoundedNetworksToAdd(originalNetworks, networksToAdd);
61+
expect(result).toHaveLength(2); // we can all networks
62+
});
63+
64+
it('returns a max size of networks to add if larger than max bounds', () => {
65+
const originalNetworks = arrangeTestNetworks(['0x1', '0x2']);
66+
const networksToAdd = arrangeTestNetworks(['0x3', '0x4']);
67+
const result = getBoundedNetworksToAdd(originalNetworks, networksToAdd, 3); // max size set to 3
68+
expect(result).toHaveLength(1); // we can only add 1 network
69+
});
70+
71+
it('returns an empty array if there is not available space to add networks', () => {
72+
const originalNetworks = arrangeTestNetworks(['0x1', '0x2']);
73+
const networksToAdd = arrangeTestNetworks(['0x3', '0x4']);
74+
75+
const result2 = getBoundedNetworksToAdd(originalNetworks, networksToAdd, 2); // max size is set to 2
76+
expect(result2).toHaveLength(0); // we've used up all the available space, so no networks can be added
77+
78+
const result3 = getBoundedNetworksToAdd(originalNetworks, networksToAdd, 1); // max size is set to 1
79+
expect(result3).toHaveLength(0); // we've used up all the available space, so no networks can be added
80+
});
81+
82+
it('returns a list of networks ordered by chainId to add', () => {
83+
const originalNetworks = arrangeTestNetworks(['0x1', '0x2']);
84+
const networksToAdd = arrangeTestNetworks(['0x3', '0x4', '0x33']);
85+
86+
const result = getBoundedNetworksToAdd(originalNetworks, networksToAdd, 4); // Max size is set to 4
87+
expect(result).toHaveLength(2); // We can only add 2 of the 3 networks to add
88+
89+
// we are only adding 0x3 and 0x33 since the list was ordered
90+
// 0x4 was dropped as we ran out of available space
91+
expect(result.map((n) => n.chainId)).toStrictEqual(['0x3', '0x33']);
92+
});
93+
94+
/**
95+
* Test Utility - creates an array of network configurations
96+
* @param chains - list of chains to create
97+
* @returns array of mock network configurations
98+
*/
99+
function arrangeTestNetworks(chains: `0x${string}`[]) {
100+
return chains.map((chainId) => {
101+
const n = createMockNetworkConfiguration();
102+
n.chainId = chainId;
103+
return n;
104+
});
105+
}
106+
});
107+
43108
describe('network-syncing/controller-integration - startNetworkSyncing()', () => {
44109
it(`should successfully sync when NetworkController:networkRemoved is emitted`, async () => {
45110
const { baseMessenger, props, deleteNetworkMock } = arrangeMocks();
@@ -237,6 +302,33 @@ describe('network-syncing/controller-integration - performMainSync()', () => {
237302
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
238303
});
239304

305+
it('should not add missing local networks if there is no available space', async () => {
306+
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
307+
arrangeMocks();
308+
mockSync.findNetworksToUpdate.mockReturnValue({
309+
remoteNetworksToUpdate: [],
310+
missingLocalNetworks: [createMockNetworkConfiguration()],
311+
localNetworksToUpdate: [],
312+
localNetworksToRemove: [],
313+
});
314+
315+
const mockAddCallback = jest.fn();
316+
await performMainNetworkSync({
317+
messenger,
318+
getStorageConfig,
319+
onNetworkAdded: mockAddCallback,
320+
maxNetworksToAdd: 0, // mocking that there is no available space
321+
});
322+
323+
expect(mockServices.mockBatchUpdateNetworks).not.toHaveBeenCalled();
324+
expect(mockCalls.mockNetworkControllerAddNetwork).not.toHaveBeenCalled();
325+
expect(mockAddCallback).not.toHaveBeenCalled();
326+
expect(
327+
mockCalls.mockNetworkControllerDangerouslySetNetworkConfiguration,
328+
).not.toHaveBeenCalled();
329+
expect(mockCalls.mockNetworkControllerRemoveNetwork).not.toHaveBeenCalled();
330+
});
331+
240332
it('should update local networks', async () => {
241333
const { messenger, getStorageConfig, mockSync, mockServices, mockCalls } =
242334
arrangeMocks();

packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { UserStorageControllerMessenger } from '../UserStorageController';
55
import { getAllRemoteNetworks } from './services';
66
import { findNetworksToUpdate } from './sync-all';
77
import { batchUpdateNetworks, deleteNetwork } from './sync-mutations';
8+
import type { NetworkConfiguration } from './types';
89

910
type StartNetworkSyncingProps = {
1011
messenger: UserStorageControllerMessenger;
@@ -15,11 +16,14 @@ type StartNetworkSyncingProps = {
1516
type PerformMainNetworkSyncProps = {
1617
messenger: UserStorageControllerMessenger;
1718
getStorageConfig: () => Promise<UserStorageBaseOptions | null>;
19+
maxNetworksToAdd?: number;
1820
onNetworkAdded?: (chainId: string) => void;
1921
onNetworkUpdated?: (chainId: string) => void;
2022
onNetworkRemoved?: (chainId: string) => void;
2123
};
2224

25+
export const MAX_NETWORKS_SIZE = 50;
26+
2327
/**
2428
* Global in-mem cache to signify that the network syncing is in progress
2529
* Ensures that listeners do not fire during main sync (prevent double requests)
@@ -72,6 +76,50 @@ export function startNetworkSyncing(props: StartNetworkSyncingProps) {
7276
}
7377
}
7478

79+
/**
80+
* Calculates the available space to add new networks
81+
* exported for testability.
82+
* @param originalListSize - size of original list
83+
* @param maxSize - max size
84+
* @returns a positive number on the available space
85+
*/
86+
export const calculateAvailableSpaceToAdd = (
87+
originalListSize: number,
88+
maxSize: number,
89+
) => {
90+
return Math.max(0, maxSize - originalListSize);
91+
};
92+
93+
/**
94+
* Returns a bounded number of networks to add (set by a max bound)
95+
* The items will be ordered to give determinism on items to append (not random)
96+
*
97+
* @param originalNetworks - The original list of network configurations.
98+
* @param networksToAdd - The list of network configurations to add.
99+
* @param maxSize - The maximum allowed size of the list. Defaults to MAX_NETWORKS_SIZE.
100+
* @returns The networks to add, sorted by chainId.
101+
*/
102+
export const getBoundedNetworksToAdd = (
103+
originalNetworks: NetworkConfiguration[],
104+
networksToAdd: NetworkConfiguration[],
105+
maxSize = MAX_NETWORKS_SIZE,
106+
) => {
107+
const availableSpace = calculateAvailableSpaceToAdd(
108+
originalNetworks.length,
109+
maxSize,
110+
);
111+
const numberOfNetworksToAppend = Math.min(
112+
availableSpace,
113+
networksToAdd.length,
114+
);
115+
116+
// Order and slice the networks to append
117+
// Ordering so we have some determinism on the order of items
118+
return networksToAdd
119+
.sort((a, b) => a.chainId.localeCompare(b.chainId))
120+
.slice(0, numberOfNetworksToAppend);
121+
};
122+
75123
/**
76124
* Action to perform the main network sync.
77125
* It will fetch local networks and remote networks, then determines which networks (local and remote) to add/update
@@ -80,7 +128,14 @@ export function startNetworkSyncing(props: StartNetworkSyncingProps) {
80128
export async function performMainNetworkSync(
81129
props: PerformMainNetworkSyncProps,
82130
) {
83-
const { messenger, getStorageConfig } = props;
131+
const {
132+
messenger,
133+
getStorageConfig,
134+
maxNetworksToAdd,
135+
onNetworkAdded,
136+
onNetworkRemoved,
137+
onNetworkUpdated,
138+
} = props;
84139

85140
// Edge-Case, we do not want to re-run the main-sync if it already is in progress
86141
/* istanbul ignore if - this is not testable */
@@ -121,15 +176,19 @@ export async function performMainNetworkSync(
121176
}
122177

123178
// Add missing local networks
124-
if (
179+
const boundedNetworkedToAdd =
125180
networkChanges?.missingLocalNetworks &&
126-
networkChanges.missingLocalNetworks.length > 0
127-
) {
181+
getBoundedNetworksToAdd(
182+
localNetworks,
183+
networkChanges.missingLocalNetworks,
184+
maxNetworksToAdd,
185+
);
186+
if (boundedNetworkedToAdd && boundedNetworkedToAdd.length > 0) {
128187
const errors: unknown[] = [];
129-
networkChanges.missingLocalNetworks.forEach((n) => {
188+
boundedNetworkedToAdd.forEach((n) => {
130189
try {
131190
messenger.call('NetworkController:addNetwork', n);
132-
props.onNetworkAdded?.(n.chainId);
191+
onNetworkAdded?.(n.chainId);
133192
} catch (e) {
134193
/* istanbul ignore next - allocates logs, do not need to test */
135194
errors.push(e);
@@ -158,7 +217,7 @@ export async function performMainNetworkSync(
158217
'NetworkController:dangerouslySetNetworkConfiguration',
159218
n,
160219
);
161-
props.onNetworkUpdated?.(n.chainId);
220+
onNetworkUpdated?.(n.chainId);
162221
} catch (e) {
163222
/* istanbul ignore next - allocates logs, do not need to test */
164223
errors.push(e);
@@ -184,7 +243,7 @@ export async function performMainNetworkSync(
184243
networkChanges.localNetworksToRemove.forEach((n) => {
185244
try {
186245
messenger.call('NetworkController:removeNetwork', n.chainId);
187-
props.onNetworkRemoved?.(n.chainId);
246+
onNetworkRemoved?.(n.chainId);
188247
} catch (e) {
189248
/* istanbul ignore next - allocates logs, do not need to test */
190249
errors.push(e);

0 commit comments

Comments
 (0)