Skip to content

Commit

Permalink
Add CronjobService and permission
Browse files Browse the repository at this point in the history
Fix build error

Fix test
  • Loading branch information
FrederikBolding authored and david0xd committed Sep 16, 2022
1 parent 664e7f7 commit c1335f9
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/controllers/package.json
Expand Up @@ -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",
Expand Down
120 changes: 120 additions & 0 deletions 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<string, unknown>;
};

export type Cronjob = {
timer?: Timer;
id: string;
} & CronjobDefinition;

export class CronjobService {
private _messenger: CronjobServiceMessenger;

private _snaps: Map<SnapId, string[]>;

private _jobs: Map<string, Cronjob>;

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 });
}
}
17 changes: 17 additions & 0 deletions 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();
});
});
41 changes: 41 additions & 0 deletions 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);
1 change: 1 addition & 0 deletions packages/controllers/src/snaps/endowments/enum.ts
Expand Up @@ -2,4 +2,5 @@ export enum SnapEndowments {
networkAccess = 'endowment:network-access',
longRunning = 'endowment:long-running',
transactionInsight = 'endowment:transaction-insight',
cronjob = 'endowment:cronjob',
}
2 changes: 2 additions & 0 deletions 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';
Expand All @@ -7,6 +8,7 @@ export const endowmentPermissionBuilders = {
[longRunningEndowmentBuilder.targetKey]: longRunningEndowmentBuilder,
[transactionInsightEndowmentBuilder.targetKey]:
transactionInsightEndowmentBuilder,
[cronjobEndowmentBuilder.targetKey]: cronjobEndowmentBuilder,
} as const;

export * from './enum';
1 change: 1 addition & 0 deletions packages/execution-environments/src/common/commands.ts
Expand Up @@ -41,6 +41,7 @@ function getHandlerArguments(
}

case HandlerType.OnRpcRequest:
case HandlerType.OnCronjob:
return { origin, request };

default:
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/types.d.ts
Expand Up @@ -18,9 +18,12 @@ export type OnTransactionHandler = (args: {
chainId: string;
}) => Promise<OnTransactionResponse>;

export type OnCronjobHandler = SnapRpcHandler;

export type SnapProvider = MetaMaskInpageProvider;

export type SnapExports = {
onRpcRequest?: OnRpcRequestHandler;
onTransaction?: OnTransactionHandler;
onCronjob?: OnCronjobHandler;
};
1 change: 1 addition & 0 deletions packages/utils/src/types.ts
Expand Up @@ -57,6 +57,7 @@ export enum SNAP_STREAM_NAMES {
export enum HandlerType {
OnRpcRequest = 'onRpcRequest',
OnTransaction = 'onTransaction',
OnCronjob = 'onCronjob',
}

export type SnapRpcHookArgs = {
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit c1335f9

Please sign in to comment.