Skip to content

Commit

Permalink
Add CronjobService and permission
Browse files Browse the repository at this point in the history
  • Loading branch information
FrederikBolding committed Jul 21, 2022
1 parent eaa5c25 commit 72aebf0
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/controllers/package.json
Expand Up @@ -42,6 +42,7 @@
"@metamask/utils": "^2.0.0",
"@types/deep-freeze-strict": "^1.1.0",
"concat-stream": "^2.0.0",
"cron-parser": "^4.5.0",
"deep-freeze-strict": "^1.1.1",
"eth-rpc-errors": "^4.0.2",
"gunzip-maybe": "^1.4.2",
Expand Down
125 changes: 125 additions & 0 deletions packages/controllers/src/services/CronjobService.ts
@@ -0,0 +1,125 @@
import {
RestrictedControllerMessenger,
HasPermission,
GetPermissions,
} from '@metamask/controllers';
import { SnapId } from '@metamask/snap-types';
import { parseExpression } from 'cron-parser';
import { nanoid } from 'nanoid';
import {
GetSnap,
HandleSnapCronjobRequest,
SnapAdded,
SnapEndowments,
} from '..';
import { Timer } from '../snaps/Timer';

export type CronjobServiceActions =
| GetSnap
| HandleSnapCronjobRequest
| 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:handleCronjobRequest',
snapId,
// @todo Decide on origin for requests like this
'METAMASK',
job.request,
);
this.schedule(snapId, job);
});
this._jobs.set(job.id, { ...job, timer });
}
}
30 changes: 30 additions & 0 deletions packages/controllers/src/snaps/SnapController.ts
Expand Up @@ -269,6 +269,14 @@ export type HandleSnapRpcRequest = {
handler: SnapController['handleRpcRequest'];
};

/**
* Handles sending an inbound cronjob message to a snap and returns its result.
*/
export type HandleSnapCronjobRequest = {
type: `${typeof controllerName}:handleCronjobRequest`;
handler: SnapController['handleCronjobRequest'];
};

/**
* Gets the specified Snap's persisted state.
*/
Expand Down Expand Up @@ -315,6 +323,7 @@ export type SnapControllerActions =
| GetSnap
| GetSnapState
| HandleSnapRpcRequest
| HandleSnapCronjobRequest
| HasSnap
| UpdateBlockedSnaps
| UpdateSnapState;
Expand Down Expand Up @@ -2083,6 +2092,27 @@ export class SnapController extends BaseController<
});
}

/**
* Passes a JSON-RPC request object to the RPC handler function of a snap, triggering the onCronjob handler.
*
* @param snapId - The ID of the recipient snap.
* @param origin - The origin of the RPC request.
* @param request - The JSON-RPC request object.
* @returns The result of the JSON-RPC request.
*/
async handleCronjobRequest(
snapId: SnapId,
origin: string,
request: Record<string, unknown>,
): Promise<unknown> {
return this.handleRequest({
snapId,
origin,
handler: HandlerType.onCronjob,
request,
});
}

/**
* Passes a JSON-RPC request object to the RPC handler function of a snap.
*
Expand Down
18 changes: 18 additions & 0 deletions packages/controllers/src/snaps/endowments/cronjob.test.ts
@@ -0,0 +1,18 @@
import { PermissionType } from '@metamask/controllers';
import { transactionInsightEndowmentBuilder } from './transaction-insight';
import { SnapEndowments } from '.';

describe('endowment:cronjob', () => {
it('builds the expected permission specification', () => {
const specification =
transactionInsightEndowmentBuilder.specificationBuilder({});
expect(specification).toStrictEqual({
permissionType: PermissionType.Endowment,
targetKey: SnapEndowments.cronjob,
endowmentGetter: expect.any(Function),
allowedCaveats: null,
});

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/enums.ts
Expand Up @@ -6,4 +6,5 @@ export enum SNAP_STREAM_NAMES {
export enum HandlerType {
onRpcRequest = 'onRpcRequest',
getTransactionInsight = 'getTransactionInsight',
onCronjob = 'onCronjob',
}
3 changes: 3 additions & 0 deletions packages/types/src/types.d.ts
Expand Up @@ -33,6 +33,8 @@ export type GetTransactionInsightHandler = (args: {
transaction: { [key: string]: unknown };
}) => Promise<string>;

export type OnCronjobHandler = SnapRpcHandler;

export type SnapProvider = MetaMaskInpageProvider;

export type SnapId = string;
Expand All @@ -46,4 +48,5 @@ export type ErrorJSON = {
export type SnapExports = {
onRpcRequest?: OnRpcRequestHandler;
getTransactionInsight?: GetTransactionInsightHandler;
onCronjob?: OnCronjobHandler;
};
17 changes: 17 additions & 0 deletions yarn.lock
Expand Up @@ -4929,6 +4929,7 @@ __metadata:
"@types/readable-stream": ^2.3.9
"@types/tar-stream": ^2.2.2
concat-stream: ^2.0.0
cron-parser: ^4.5.0
deep-freeze-strict: ^1.1.1
eslint: ^7.30.0
eslint-config-prettier: ^8.3.0
Expand Down Expand Up @@ -9092,6 +9093,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 @@ -14986,6 +14996,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 72aebf0

Please sign in to comment.