From c1335f9a1843ea8aac367e5fc8690fce672ad361 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 21 Jul 2022 12:58:34 +0200 Subject: [PATCH] Add CronjobService and permission Fix build error Fix test --- packages/controllers/package.json | 1 + .../src/services/CronjobService.ts | 120 ++++++++++++++++++ .../src/snaps/endowments/cronjob.test.ts | 17 +++ .../src/snaps/endowments/cronjob.ts | 41 ++++++ .../controllers/src/snaps/endowments/enum.ts | 1 + .../controllers/src/snaps/endowments/index.ts | 2 + .../src/common/commands.ts | 1 + packages/types/src/types.d.ts | 3 + packages/utils/src/types.ts | 1 + yarn.lock | 17 +++ 10 files changed, 204 insertions(+) create mode 100644 packages/controllers/src/services/CronjobService.ts create mode 100644 packages/controllers/src/snaps/endowments/cronjob.test.ts create mode 100644 packages/controllers/src/snaps/endowments/cronjob.ts diff --git a/packages/controllers/package.json b/packages/controllers/package.json index 0c07ffcd84..6a714d2a2b 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -41,6 +41,7 @@ "@metamask/utils": "^2.0.0", "@xstate/fsm": "^2.0.0", "concat-stream": "^2.0.0", + "cron-parser": "^4.5.0", "eth-rpc-errors": "^4.0.2", "gunzip-maybe": "^1.4.2", "immer": "^9.0.6", diff --git a/packages/controllers/src/services/CronjobService.ts b/packages/controllers/src/services/CronjobService.ts new file mode 100644 index 0000000000..627301dc2d --- /dev/null +++ b/packages/controllers/src/services/CronjobService.ts @@ -0,0 +1,120 @@ +import { + RestrictedControllerMessenger, + HasPermission, + GetPermissions, +} from '@metamask/controllers'; +import { HandlerType, SnapId } from '@metamask/snap-utils'; +import { parseExpression } from 'cron-parser'; +import { nanoid } from 'nanoid'; +import { GetSnap, HandleSnapRequest, SnapAdded, SnapEndowments } from '..'; +import { Timer } from '../snaps/Timer'; + +export type CronjobServiceActions = + | GetSnap + | HandleSnapRequest + | HasPermission + | GetPermissions; + +export type CronjobServiceEvents = SnapAdded; + +export type CronjobServiceMessenger = RestrictedControllerMessenger< + 'CronjobService', + CronjobServiceActions, + CronjobServiceEvents, + CronjobServiceActions['type'], + CronjobServiceEvents['type'] +>; + +export type CronjobServiceArgs = { + messenger: CronjobServiceMessenger; +}; + +export type CronjobData = { + jobs: Cronjob[]; +}; + +export type CronjobDefinition = { + expression: string; + request: Record; +}; + +export type Cronjob = { + timer?: Timer; + id: string; +} & CronjobDefinition; + +export class CronjobService { + private _messenger: CronjobServiceMessenger; + + private _snaps: Map; + + private _jobs: Map; + + constructor({ messenger }: CronjobServiceArgs) { + this._snaps = new Map(); + this._jobs = new Map(); + this._messenger = messenger; + } + + async register(snapId: SnapId) { + const hasCronjob = await this._messenger.call( + 'PermissionController:hasPermission', + snapId, + SnapEndowments.cronjob, + ); + if (!hasCronjob) { + return; + } + const permissions = await this._messenger.call( + 'PermissionController:getPermissions', + snapId, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const cronjobPermission = permissions![SnapEndowments.cronjob]!; + // @todo Figure out how to get this from the permission + const definitions: CronjobDefinition[] = (cronjobPermission as any).jobs; + + const jobs = definitions.map((definition) => { + const id = nanoid(); + return { ...definition, id }; + }); + + this._snaps.set( + snapId, + jobs.map((job) => job.id), + ); + + jobs.forEach((job) => this.schedule(snapId, job)); + } + + unregister(snapId: SnapId) { + const jobs = this._snaps.get(snapId); + jobs?.forEach((id) => { + const job = this._jobs.get(id); + job?.timer?.cancel(); + }); + this._snaps.delete(snapId); + } + + schedule(snapId: SnapId, job: Cronjob) { + const parsed = parseExpression(job.expression); + if (!parsed.hasNext()) { + return; + } + const next = parsed.next(); + const now = new Date(); + const ms = now.getTime() - next.getTime(); + const timer = new Timer(ms); + timer.start(() => { + this._messenger.call('SnapController:handleRequest', { + snapId, + // @todo Decide on origin for requests like this + origin: 'METAMASK', + handler: HandlerType.OnCronjob, + request: job.request, + }); + this.schedule(snapId, job); + }); + this._jobs.set(job.id, { ...job, timer }); + } +} diff --git a/packages/controllers/src/snaps/endowments/cronjob.test.ts b/packages/controllers/src/snaps/endowments/cronjob.test.ts new file mode 100644 index 0000000000..9a662ddfe0 --- /dev/null +++ b/packages/controllers/src/snaps/endowments/cronjob.test.ts @@ -0,0 +1,17 @@ +import { PermissionType } from '@metamask/controllers'; +import { cronjobEndowmentBuilder } from './cronjob'; +import { SnapEndowments } from '.'; + +describe('endowment:cronjob', () => { + it('builds the expected permission specification', () => { + const specification = cronjobEndowmentBuilder.specificationBuilder({}); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetKey: SnapEndowments.cronjob, + endowmentGetter: expect.any(Function), + allowedCaveats: ['cronjobCaveat'], + }); + + expect(specification.endowmentGetter()).toBeUndefined(); + }); +}); diff --git a/packages/controllers/src/snaps/endowments/cronjob.ts b/packages/controllers/src/snaps/endowments/cronjob.ts new file mode 100644 index 0000000000..d5bd49cc10 --- /dev/null +++ b/packages/controllers/src/snaps/endowments/cronjob.ts @@ -0,0 +1,41 @@ +import { + PermissionSpecificationBuilder, + PermissionType, + EndowmentGetterParams, + ValidPermissionSpecification, +} from '@metamask/controllers'; +import { SnapEndowments } from './enum'; + +const permissionName = SnapEndowments.cronjob; + +type CronjobEndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetKey: typeof permissionName; + endowmentGetter: (_options?: any) => undefined; + allowedCaveats: null; +}>; + +/** + * `endowment:cronjob` returns nothing; it is intended to be used as a flag to determine whether the snap wants to run cronjobs. + * + * @param _builderOptions - Optional specification builder options. + * @returns The specification for the cronjob endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + any, + CronjobEndowmentSpecification +> = (_builderOptions?: any) => { + return { + permissionType: PermissionType.Endowment, + targetKey: permissionName, + // @todo not allowed by types? + allowedCaveats: ['cronjobCaveat'] as any, + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => undefined, + }; +}; + +export const cronjobEndowmentBuilder = Object.freeze({ + targetKey: permissionName, + specificationBuilder, +} as const); diff --git a/packages/controllers/src/snaps/endowments/enum.ts b/packages/controllers/src/snaps/endowments/enum.ts index 82a4dfda4c..aed6d07b3f 100644 --- a/packages/controllers/src/snaps/endowments/enum.ts +++ b/packages/controllers/src/snaps/endowments/enum.ts @@ -2,4 +2,5 @@ export enum SnapEndowments { networkAccess = 'endowment:network-access', longRunning = 'endowment:long-running', transactionInsight = 'endowment:transaction-insight', + cronjob = 'endowment:cronjob', } diff --git a/packages/controllers/src/snaps/endowments/index.ts b/packages/controllers/src/snaps/endowments/index.ts index 93ac7f99b8..8472b02ddd 100644 --- a/packages/controllers/src/snaps/endowments/index.ts +++ b/packages/controllers/src/snaps/endowments/index.ts @@ -1,3 +1,4 @@ +import { cronjobEndowmentBuilder } from './cronjob'; import { longRunningEndowmentBuilder } from './long-running'; import { networkAccessEndowmentBuilder } from './network-access'; import { transactionInsightEndowmentBuilder } from './transaction-insight'; @@ -7,6 +8,7 @@ export const endowmentPermissionBuilders = { [longRunningEndowmentBuilder.targetKey]: longRunningEndowmentBuilder, [transactionInsightEndowmentBuilder.targetKey]: transactionInsightEndowmentBuilder, + [cronjobEndowmentBuilder.targetKey]: cronjobEndowmentBuilder, } as const; export * from './enum'; diff --git a/packages/execution-environments/src/common/commands.ts b/packages/execution-environments/src/common/commands.ts index 68335de454..40cc811d73 100644 --- a/packages/execution-environments/src/common/commands.ts +++ b/packages/execution-environments/src/common/commands.ts @@ -41,6 +41,7 @@ function getHandlerArguments( } case HandlerType.OnRpcRequest: + case HandlerType.OnCronjob: return { origin, request }; default: diff --git a/packages/types/src/types.d.ts b/packages/types/src/types.d.ts index 2e9a355035..871396a573 100644 --- a/packages/types/src/types.d.ts +++ b/packages/types/src/types.d.ts @@ -18,9 +18,12 @@ export type OnTransactionHandler = (args: { chainId: string; }) => Promise; +export type OnCronjobHandler = SnapRpcHandler; + export type SnapProvider = MetaMaskInpageProvider; export type SnapExports = { onRpcRequest?: OnRpcRequestHandler; onTransaction?: OnTransactionHandler; + onCronjob?: OnCronjobHandler; }; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index fba113dbdb..cb8b88fa92 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -57,6 +57,7 @@ export enum SNAP_STREAM_NAMES { export enum HandlerType { OnRpcRequest = 'onRpcRequest', OnTransaction = 'onTransaction', + OnCronjob = 'onCronjob', } export type SnapRpcHookArgs = { diff --git a/yarn.lock b/yarn.lock index 2d7c7f6119..f6cca02710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2945,6 +2945,7 @@ __metadata: "@types/tar-stream": ^2.2.2 "@xstate/fsm": ^2.0.0 concat-stream: ^2.0.0 + cron-parser: ^4.5.0 eslint: ^7.30.0 eslint-config-prettier: ^8.3.0 eslint-plugin-import: ^2.23.4 @@ -6975,6 +6976,15 @@ __metadata: languageName: node linkType: hard +"cron-parser@npm:^4.5.0": + version: 4.5.0 + resolution: "cron-parser@npm:4.5.0" + dependencies: + luxon: ^2.4.0 + checksum: 9e5a6d07c2d86fb27b5701067018776aaf9ad4bf9f57a0f02e5f7c33d3d46dd804802ed74c54f001b18db540293633f1904632efdab3466e1f5630b953de26eb + languageName: node + linkType: hard + "cross-fetch@npm:^2.1.0": version: 2.2.6 resolution: "cross-fetch@npm:2.2.6" @@ -12915,6 +12925,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^2.4.0": + version: 2.5.0 + resolution: "luxon@npm:2.5.0" + checksum: 2fccce6bbdfc8f13c5a8c148ff045ab3b10f4f80cac28dd92575588fffce9b2d7197096d7fedcc61a6245b59f4233507797f530e63f22b9ae4c425dff2909ae3 + languageName: node + linkType: hard + "magic-string@npm:0.25.1": version: 0.25.1 resolution: "magic-string@npm:0.25.1"