Skip to content

Commit

Permalink
address PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
hmalik88 committed Dec 13, 2024
1 parent 757b5a2 commit 528d3f0
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 56 deletions.
2 changes: 1 addition & 1 deletion packages/examples/packages/cronjobs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => {

/**
* Handle incoming JSON-RPC requests from the dapp, sent through the
* `wallet_invokeSnap` method. This handler handles two methods:
* `wallet_invokeSnap` method. This handler handles three methods:
*
* - `scheduleNotification`: Schedule a notification in the future.
* - `cancelNotification`: Cancel a notification.
Expand Down
27 changes: 27 additions & 0 deletions packages/snaps-controllers/src/cronjob/CronjobController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,33 @@ describe('CronjobController', () => {
cronjobController.destroy();
});

it('fails to schedule a background event if the date is in the past', () => {
const rootMessenger = getRootCronjobControllerMessenger();
const controllerMessenger =
getRestrictedCronjobControllerMessenger(rootMessenger);

const cronjobController = new CronjobController({
messenger: controllerMessenger,
});

const backgroundEvent = {
snapId: MOCK_SNAP_ID,
date: '2021-01-01T01:00Z',
request: {
method: 'handleEvent',
params: ['p1'],
},
};

expect(() =>
cronjobController.scheduleBackgroundEvent(backgroundEvent),
).toThrow('Cannot schedule an event in the past.');

expect(cronjobController.state.events).toStrictEqual({});

cronjobController.destroy();
});

it('cancels a background event', () => {
const rootMessenger = getRootCronjobControllerMessenger();
const controllerMessenger =
Expand Down
8 changes: 6 additions & 2 deletions packages/snaps-controllers/src/cronjob/CronjobController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,10 @@ export class CronjobController extends BaseController<
scheduleBackgroundEvent(
backgroundEventWithoutId: Omit<BackgroundEvent, 'id' | 'scheduledAt'>,
) {
// removing minute precision and converting to UTC.
// removing milliseond precision and converting to UTC.
const scheduledAt = DateTime.fromJSDate(new Date())
.toUTC()
.toFormat("yyyy-MM-dd'T'HH:mm'Z'");
.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
const event = {
...backgroundEventWithoutId,
id: nanoid(),
Expand Down Expand Up @@ -394,6 +394,10 @@ export class CronjobController extends BaseController<
const now = new Date();
const ms = date.getTime() - now.getTime();

if (ms < 0) {
throw new Error('Cannot schedule an event in the past.');
}

const timer = new Timer(ms);
timer.start(() => {
this.#messenger
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 92.83,
functions: 97.36,
lines: 97.92,
statements: 97.47,
branches: 93.23,
functions: 97.89,
lines: 98.48,
statements: 98.01,
},
},
});
123 changes: 122 additions & 1 deletion packages/snaps-rpc-methods/src/endowments/cronjob.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { Caveat } from '@metamask/permission-controller';
import type {
Caveat,
PermissionConstraint,
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { SnapCaveatType } from '@metamask/snaps-utils';

Expand All @@ -7,6 +10,7 @@ import {
cronjobEndowmentBuilder,
validateCronjobCaveat,
cronjobCaveatSpecifications,
getCronjobCaveatJobs,
} from './cronjob';
import { SnapEndowments } from './enum';

Expand Down Expand Up @@ -62,6 +66,123 @@ describe('endowment:cronjob', () => {
});
});

describe('getCronjobCaveatJobs', () => {
it('returns the jobs from a cronjob caveat', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.SnapCronjob,
value: {
jobs: [
{
expression: '* * * * *',
request: {
method: 'exampleMethodOne',
params: ['p1'],
},
},
],
},
},
],
};

expect(getCronjobCaveatJobs(permission)).toStrictEqual([
{
expression: '* * * * *',
request: {
method: 'exampleMethodOne',
params: ['p1'],
},
},
]);
});

it('returns null if there are no caveats', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: null,
};

expect(getCronjobCaveatJobs(permission)).toBeNull();
});

it('will throw if there is more than one caveat', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.SnapCronjob,
value: {
jobs: [
{
expression: '* * * * *',
request: {
method: 'exampleMethodOne',
params: ['p1'],
},
},
],
},
},
{
type: SnapCaveatType.SnapCronjob,
value: {
jobs: [
{
expression: '* * * * *',
request: {
method: 'exampleMethodOne',
params: ['p1'],
},
},
],
},
},
],
};

expect(() => getCronjobCaveatJobs(permission)).toThrow('Assertion failed.');
});

it('will throw if the caveat type is wrong', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.ChainIds,
value: {
jobs: [
{
expression: '* * * * *',
request: {
method: 'exampleMethodOne',
params: ['p1'],
},
},
],
},
},
],
};

expect(() => getCronjobCaveatJobs(permission)).toThrow('Assertion failed.');
});
});

describe('validateCronjobCaveat', () => {
it('should not throw an error when provided specification is valid', () => {
const caveat: Caveat<string, any> = {
Expand Down
15 changes: 7 additions & 8 deletions packages/snaps-rpc-methods/src/endowments/cronjob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export const cronjobEndowmentBuilder = Object.freeze({
*/
export function getCronjobCaveatMapper(
value: Json,
): Pick<PermissionConstraint, 'caveats'> {
): Pick<PermissionConstraint, 'caveats'> | Record<string, never> {
if (Object.keys(value as Record<string, Json>).length === 0) {
return {};
}
return {
caveats: [
{
Expand Down Expand Up @@ -96,7 +99,7 @@ export function getCronjobCaveatJobs(

const caveat = permission.caveats[0] as Caveat<string, { jobs: Json[] }>;

return (caveat.value?.jobs as CronjobSpecification[]) ?? [];
return (caveat.value?.jobs as CronjobSpecification[]) ?? null;
}

/**
Expand All @@ -116,17 +119,13 @@ export function validateCronjobCaveat(caveat: Caveat<string, any>) {

const { value } = caveat;

if (!isPlainObject(value)) {
if (!hasProperty(value, 'jobs') || !isPlainObject(value)) {
throw rpcErrors.invalidParams({
message: 'Expected a plain object.',
});
}

const valueKeys = Object.keys(value);

// If it's an empty object, ok to skip validation as this indicates
// intention to use the background event rpc methods
if (valueKeys.length !== 0 && !isCronjobSpecificationArray(value.jobs)) {
if (!isCronjobSpecificationArray(value.jobs)) {
throw rpcErrors.invalidParams({
message: 'Expected a valid cronjob specification array.',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';

import { SnapEndowments } from '../endowments';
import { cancelBackgroundEventHandler } from './cancelBackgroundEvent';

describe('snap_cancelBackgroundEvent', () => {
Expand Down Expand Up @@ -142,8 +141,9 @@ describe('snap_cancelBackgroundEvent', () => {

expect(response).toStrictEqual({
error: {
code: -32600,
message: `The snap "${MOCK_SNAP_ID}" does not have the "${SnapEndowments.Cronjob}" permission.`,
code: 4100,
message:
'The requested account and/or method has not been authorized by the user.',
stack: expect.any(String),
},
id: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
import type { PermittedHandlerExport } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import type {
JsonRpcRequest,
CancelBackgroundEventParams,
Expand Down Expand Up @@ -64,14 +64,10 @@ async function getCancelBackgroundEventImplementation(
end: JsonRpcEngineEndCallback,
{ cancelBackgroundEvent, hasPermission }: CancelBackgroundEventMethodHooks,
): Promise<void> {
const { params, origin } = req as JsonRpcRequest & { origin: string };
const { params } = req;

if (!hasPermission(SnapEndowments.Cronjob)) {
return end(
rpcErrors.invalidRequest({
message: `The snap "${origin}" does not have the "${SnapEndowments.Cronjob}" permission.`,
}),
);
return end(providerErrors.unauthorized());
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';

import { SnapEndowments } from '../endowments';
import { getBackgroundEventsHandler } from './getBackgroundEvents';

describe('snap_getBackgroundEvents', () => {
Expand Down Expand Up @@ -207,8 +206,9 @@ describe('snap_getBackgroundEvents', () => {

expect(response).toStrictEqual({
error: {
code: -32600,
message: `The snap "${MOCK_SNAP_ID}" does not have the "${SnapEndowments.Cronjob}" permission.`,
code: 4100,
message:
'The requested account and/or method has not been authorized by the user.',
stack: expect.any(String),
},
id: 1,
Expand Down
14 changes: 4 additions & 10 deletions packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
import type { PermittedHandlerExport } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import { providerErrors } from '@metamask/rpc-errors';
import type {
BackgroundEvent,
GetBackgroundEventsParams,
Expand Down Expand Up @@ -37,7 +37,7 @@ export const getBackgroundEventsHandler: PermittedHandlerExport<
/**
* The `snap_getBackgroundEvents` method implementation.
*
* @param req - The JSON-RPC request object.
* @param _req - The JSON-RPC request object. Not used by this function.
* @param res - The JSON-RPC response object.
* @param _next - The `json-rpc-engine` "next" callback.
* Not used by this function.
Expand All @@ -48,20 +48,14 @@ export const getBackgroundEventsHandler: PermittedHandlerExport<
* @returns An array of background events.
*/
async function getGetBackgroundEventsImplementation(
req: JsonRpcRequest<GetBackgroundEventsParams>,
_req: JsonRpcRequest<GetBackgroundEventsParams>,
res: PendingJsonRpcResponse<GetBackgroundEventsResult>,
_next: unknown,
end: JsonRpcEngineEndCallback,
{ getBackgroundEvents, hasPermission }: GetBackgroundEventsMethodHooks,
): Promise<void> {
const { origin } = req as JsonRpcRequest & { origin: string };

if (!hasPermission(SnapEndowments.Cronjob)) {
return end(
rpcErrors.invalidRequest({
message: `The snap "${origin}" does not have the "${SnapEndowments.Cronjob}" permission.`,
}),
);
return end(providerErrors.unauthorized());
}
try {
const events = getBackgroundEvents();
Expand Down
Loading

0 comments on commit 528d3f0

Please sign in to comment.