Skip to content

Commit

Permalink
Move rate limit controller to init function
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Feb 3, 2025
1 parent b068d1d commit 6699f5f
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 61 deletions.
6 changes: 6 additions & 0 deletions app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
SnapInsightsController,
SnapInterfaceController,
} from '@metamask/snaps-controllers';
import {
RateLimitController,
RateLimitedApiMap,
} from '@metamask/rate-limit-controller';
import OnboardingController from '../controllers/onboarding';
import { PreferencesController } from '../controllers/preferences-controller';
import SwapsController from '../controllers/swaps';
Expand All @@ -38,6 +42,7 @@ export type Controller =
>
| PPOMController
| PreferencesController
| RateLimitController<RateLimitedApiMap>
| SmartTransactionsController
// TODO: Update `name` to `SnapController` instead of `string`.
| SnapController
Expand Down Expand Up @@ -66,6 +71,7 @@ export type ControllerFlatState = AccountsController['state'] &
>['state'] &
PPOMController['state'] &
PreferencesController['state'] &
RateLimitController<RateLimitedApiMap>['state'] &
SmartTransactionsController['state'] &
SnapController['state'] &
SnapInsightsController['state'] &
Expand Down
6 changes: 6 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
getCronjobControllerMessenger,
getRateLimitControllerInitMessenger,
getRateLimitControllerMessenger,
getSnapControllerInitMessenger,
getSnapControllerMessenger,
getSnapInsightsControllerMessenger,
Expand All @@ -19,6 +21,10 @@ export const CONTROLLER_MESSENGERS = {
CronjobController: {
getMessenger: getCronjobControllerMessenger,
},
RateLimitController: {
getMessenger: getRateLimitControllerMessenger,
getInitMessenger: getRateLimitControllerInitMessenger,
},
SnapsRegistry: {
getMessenger: getSnapsRegistryMessenger,
},
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/controller-init/snaps/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export { CronjobControllerInit } from './cronjob-controller-init';
export { getCronjobControllerMessenger } from './cronjob-controller-messenger';
export { RateLimitControllerInit } from './rate-limit-controller-init';
export {
getRateLimitControllerMessenger,
getRateLimitControllerInitMessenger,
} from './rate-limit-controller-messenger';
export { SnapControllerInit } from './snap-controller-init';
export {
getSnapControllerMessenger,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ControllerMessenger } from '@metamask/base-controller';
import { ControllerInitRequest } from '../types';
import { buildControllerInitRequestMock } from '../test/utils';
import { RateLimitControllerInit } from './rate-limit-controller-init';
import { RateLimitController } from '@metamask/rate-limit-controller';

Check failure on line 5 in app/scripts/controller-init/snaps/rate-limit-controller-init.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

`@metamask/rate-limit-controller` import should occur before import of `../types`
import {
getRateLimitControllerInitMessenger,
getRateLimitControllerMessenger,
RateLimitControllerInitMessenger,
RateLimitControllerMessenger,
} from './rate-limit-controller-messenger';

jest.mock('@metamask/rate-limit-controller');

function getInitRequestMock(): jest.Mocked<
ControllerInitRequest<RateLimitControllerMessenger, RateLimitControllerInitMessenger>

Check failure on line 16 in app/scripts/controller-init/snaps/rate-limit-controller-init.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Replace `RateLimitControllerMessenger,·RateLimitControllerInitMessenger` with `⏎····RateLimitControllerMessenger,⏎····RateLimitControllerInitMessenger⏎··`
> {
const baseControllerMessenger = new ControllerMessenger<never, never>();

const requestMock = {
...buildControllerInitRequestMock(),
controllerMessenger: getRateLimitControllerMessenger(baseControllerMessenger),

Check failure on line 22 in app/scripts/controller-init/snaps/rate-limit-controller-init.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Replace `baseControllerMessenger` with `⏎······baseControllerMessenger,⏎····`
initMessenger: getRateLimitControllerInitMessenger(baseControllerMessenger),
};

return requestMock;
}

describe('RateLimitController', () => {
it('initializes the controller', () => {
const { controller } = RateLimitControllerInit(getInitRequestMock());
expect(controller).toBeInstanceOf(RateLimitController);
});

it('passes the proper arguments to the controller', () => {
RateLimitControllerInit(getInitRequestMock());

const controllerMock = jest.mocked(RateLimitController);
expect(controllerMock).toHaveBeenCalledWith({
messenger: expect.any(Object),
implementations: {
showInAppNotification: {
method: expect.any(Function),
rateLimitCount: 5,
rateLimitTimeout: 60_000,
},
showNativeNotification: {
method: expect.any(Function),
rateLimitCount: 2,
rateLimitTimeout: 300_000,
}

Check failure on line 51 in app/scripts/controller-init/snaps/rate-limit-controller-init.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Insert `,`
}

Check failure on line 52 in app/scripts/controller-init/snaps/rate-limit-controller-init.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Insert `,`
});
});
});
108 changes: 108 additions & 0 deletions app/scripts/controller-init/snaps/rate-limit-controller-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
RateLimitController,
RateLimitedApiMap,
} from '@metamask/rate-limit-controller';
import log from 'loglevel';
import { TRIGGER_TYPES } from '@metamask/notification-services-controller/notification-services';
import { ControllerInitFunction } from '../types';
import {
RateLimitControllerInitMessenger,
RateLimitControllerMessenger,
} from './rate-limit-controller-messenger';

/**
* Initialize the Snap insights controller.
*
* @param request - The request object.
* @param request.controllerMessenger - The controller messenger to use for the
* controller.
* @param request.persistedState - The persisted state of the extension.
* @param request.initMessenger - The init messenger. This has access to
* different functions than the controller messenger, and should be used for
* initialization purposes only.
* @param request.showNotification - Function to show a notification.
* @returns The initialized controller.
*/
export const RateLimitControllerInit: ControllerInitFunction<
RateLimitController<RateLimitedApiMap>,
RateLimitControllerMessenger,
RateLimitControllerInitMessenger
> = ({
controllerMessenger,
initMessenger,
persistedState,
showNotification,
}) => {
const controller = new RateLimitController({
state: persistedState.RateLimitController,
// @ts-expect-error: Property `#private` in type` RestrictedMessenger`
// refers to a different member that cannot be accessed from within type
// `RestrictedControllerMessenger`.
// TODO: Remove this when switched to `RestrictedMessenger`.
messenger: controllerMessenger,

implementations: {
showNativeNotification: {
method: (origin, message) => {
const subjectMetadataState = initMessenger.call(
'SubjectMetadataController:getState',
);

const originMetadata = subjectMetadataState.subjectMetadata[origin];

showNotification(originMetadata?.name ?? origin, message).catch(
(error: unknown) => {
log.error('Failed to create notification', error);
},
);

return null;
},

// 2 calls per 5 minutes
rateLimitCount: 2,
rateLimitTimeout: 300_000,
},

showInAppNotification: {
method: (origin, args) => {
const { message, title, footerLink, interfaceId } = args;

const detailedView = {
title,
...(footerLink ? { footerLink } : {}),
interfaceId,
};

const notification = {
data: {
message,
origin,
...(interfaceId ? { detailedView } : {}),
},
type: TRIGGER_TYPES.SNAP,
readDate: null,
};

initMessenger.call(
'NotificationServicesController:updateMetamaskNotificationsList',
// @ts-expect-error: `notification` is not compatible with the
// expected type.
// TODO: Look into the type mismatch.
notification,
);

return null;
},

// 5 calls per minute
rateLimitCount: 5,
rateLimitTimeout: 60_000,
},
},
});

return {
controller,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
ControllerMessenger,
RestrictedMessenger,
} from '@metamask/base-controller';
import {
getRateLimitControllerInitMessenger,
getRateLimitControllerMessenger,
} from './rate-limit-controller-messenger';

describe('getRateLimitControllerMessenger', () => {
it('returns a restricted controller messenger', () => {
const controllerMessenger = new ControllerMessenger<never, never>();
const rateLimitControllerMessenger =
getRateLimitControllerMessenger(controllerMessenger);

expect(rateLimitControllerMessenger).toBeInstanceOf(RestrictedMessenger);
});
});

describe('getRateLimitControllerInitMessenger', () => {
it('returns a restricted controller messenger', () => {
const controllerMessenger = new ControllerMessenger<never, never>();
const rateLimitControllerInitMessenger =
getRateLimitControllerInitMessenger(controllerMessenger);

expect(rateLimitControllerInitMessenger).toBeInstanceOf(RestrictedMessenger);

Check failure on line 26 in app/scripts/controller-init/snaps/rate-limit-controller-messenger.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Replace `RestrictedMessenger` with `⏎······RestrictedMessenger,⏎····`
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ControllerMessenger } from '@metamask/base-controller';
import {
SnapInstalled,
SnapUpdated,
SnapDisabled,
SnapEnabled,
SnapUninstalled,
HandleSnapRequest,
GetAllSnaps,
} from '@metamask/snaps-controllers';
import {
GetPermissions,
GetSubjectMetadataState,
} from '@metamask/permission-controller';
import { NotificationServicesControllerUpdateMetamaskNotificationsList } from '@metamask/notification-services-controller/notification-services';

type Actions = GetPermissions | HandleSnapRequest | GetAllSnaps;

type Events =
| SnapInstalled
| SnapUpdated
| SnapUninstalled
| SnapEnabled
| SnapDisabled;

export type RateLimitControllerMessenger = ReturnType<
typeof getRateLimitControllerMessenger
>;

/**
* Get a restricted controller messenger for the rate limit controller. This is
* scoped to the actions and events that the rate limit controller is allowed to
* handle.
*
* @param controllerMessenger - The controller messenger to restrict.
* @returns The restricted controller messenger.
*/
export function getRateLimitControllerMessenger(
controllerMessenger: ControllerMessenger<Actions, Events>,
) {
return controllerMessenger.getRestricted({
name: 'RateLimitController',
allowedEvents: [],
allowedActions: [],
});
}

type InitActions =
| GetSubjectMetadataState
| NotificationServicesControllerUpdateMetamaskNotificationsList;

export type RateLimitControllerInitMessenger = ReturnType<
typeof getRateLimitControllerInitMessenger
>;

/**
* Get a restricted controller messenger for the rate limit controller. This is
* scoped to the actions and events that the rate limit controller is allowed to
* handle.
*
* @param controllerMessenger - The controller messenger to restrict.
* @returns The restricted controller messenger.
*/
export function getRateLimitControllerInitMessenger(
controllerMessenger: ControllerMessenger<InitActions, never>,
) {
return controllerMessenger.getRestricted({
name: 'RateLimitController',
allowedActions: [
'SubjectMetadataController:getState',
'NotificationServicesController:updateMetamaskNotificationsList',
],
allowedEvents: [],
});
}
13 changes: 13 additions & 0 deletions app/scripts/controller-init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ export type ControllerInitRequest<
* @param origin - The origin for which to remove all connections.
*/
removeAllConnections(origin: string): void;

/**
* Show a native notification.
*
* @param title - The title of the notification.
* @param message - The message of the notification.
* @param url - The URL to open when the notification is clicked.
*/
showNotification: (
title: string,
message: string,
url?: string,
) => Promise<void>;
} & (InitMessengerType extends BaseRestrictedControllerMessenger
? {
/**
Expand Down
Loading

0 comments on commit 6699f5f

Please sign in to comment.