Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 94.41,
"branches": 94.42,
"functions": 98.4,
"lines": 98.68,
"lines": 98.69,
"statements": 98.52
}
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ describe('CronjobController', () => {

const backgroundEvent = {
snapId: MOCK_SNAP_ID,
date: '2022-01-01T01:00Z',
date: '2025-05-21T13:25:21.500Z',
request: {
method: 'handleEvent',
params: ['p1'],
Expand All @@ -449,7 +449,7 @@ describe('CronjobController', () => {
{
id,
snapId: MOCK_SNAP_ID,
date: '2022-01-01T01:00Z',
date: '2025-05-21T13:25:21Z',
request: {
method: 'handleEvent',
params: ['p1'],
Expand Down Expand Up @@ -1023,7 +1023,7 @@ describe('CronjobController', () => {
'CronjobController:scheduleBackgroundEvent',
{
snapId: MOCK_SNAP_ID,
date: '2022-01-01T01:00Z',
date: '2025-05-21T13:25:21.500Z',
request: {
method: 'handleExport',
params: ['p1'],
Expand All @@ -1036,7 +1036,7 @@ describe('CronjobController', () => {
id,
snapId: MOCK_SNAP_ID,
scheduledAt: expect.any(String),
date: '2022-01-01T01:00Z',
date: '2025-05-21T13:25:21.500Z',
request: {
method: 'handleExport',
params: ['p1'],
Expand All @@ -1054,7 +1054,7 @@ describe('CronjobController', () => {
id,
snapId: MOCK_SNAP_ID,
scheduledAt: expect.any(String),
date: '2022-01-01T01:00Z',
date: '2025-05-21T13:25:21Z',
request: {
method: 'handleExport',
params: ['p1'],
Expand Down
26 changes: 19 additions & 7 deletions packages/snaps-controllers/src/cronjob/CronjobController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
logError,
logWarning,
} from '@metamask/snaps-utils';
import { assert, Duration, inMilliseconds } from '@metamask/utils';
import { assert, Duration, hasProperty, inMilliseconds } from '@metamask/utils';
import { castDraft } from 'immer';
import { DateTime } from 'luxon';
import { nanoid } from 'nanoid';
Expand Down Expand Up @@ -331,9 +331,6 @@ export class CronjobController extends BaseController<
};

this.#setUpBackgroundEvent(event);
this.update((state) => {
state.events[event.id] = castDraft(event);
});

return event.id;
}
Expand Down Expand Up @@ -379,6 +376,13 @@ export class CronjobController extends BaseController<
throw new Error('Cannot schedule an event in the past.');
}

// The event may already be in state when we get here.
if (!hasProperty(this.state.events, event.id)) {
this.update((state) => {
state.events[event.id] = castDraft(event);
});
}

const timer = new Timer(ms);
timer.start(() => {
this.messagingSystem
Expand Down Expand Up @@ -410,9 +414,17 @@ export class CronjobController extends BaseController<
* @returns An array of background events.
*/
getBackgroundEvents(snapId: SnapId): BackgroundEvent[] {
return Object.values(this.state.events).filter(
(snapEvent) => snapEvent.snapId === snapId,
);
return Object.values(this.state.events)
.filter((snapEvent) => snapEvent.snapId === snapId)
.map((event) => ({
...event,
// Truncate dates to remove milliseconds.
date: DateTime.fromISO(event.date, { setZone: true })
.startOf('second')
.toISO({
suppressMilliseconds: true,
}) as string,
}));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 95,
branches: 95.03,
functions: 98.65,
lines: 98.78,
statements: 98.47,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ describe('snap_scheduleBackgroundEvent', () => {
});

describe('implementation', () => {
beforeAll(() => {
jest.useFakeTimers();

// Specifically setting a Date that is in between two seconds.
jest.setSystemTime(1747833920500);
});

afterAll(() => {
jest.useRealTimers();
});

const createOriginMiddleware =
(origin: string) =>
(request: any, _response: unknown, next: () => void, _end: unknown) => {
Expand Down Expand Up @@ -151,7 +162,55 @@ describe('snap_scheduleBackgroundEvent', () => {
id: 1,
method: 'snap_scheduleBackgroundEvent',
params: {
duration: 'PT30S',
duration: 'PT1S',
request: {
method: 'handleExport',
params: ['p1'],
},
},
});

expect(scheduleBackgroundEvent).toHaveBeenCalledWith({
date: '2025-05-21T13:25:21.500Z',
request: {
method: 'handleExport',
params: ['p1'],
},
});
});

it('schedules a background event using a minimum duration of 1 second', async () => {
const { implementation } = scheduleBackgroundEventHandler;

const scheduleBackgroundEvent = jest.fn();
const hasPermission = jest.fn().mockImplementation(() => true);

const hooks = {
scheduleBackgroundEvent,
hasPermission,
};

const engine = new JsonRpcEngine();

engine.push(createOriginMiddleware(MOCK_SNAP_ID));
engine.push((request, response, next, end) => {
const result = implementation(
request as JsonRpcRequest<ScheduleBackgroundEventParams>,
response as PendingJsonRpcResponse<ScheduleBackgroundEventResult>,
next,
end,
hooks,
);

result?.catch(end);
});

await engine.handle({
jsonrpc: '2.0',
id: 1,
method: 'snap_scheduleBackgroundEvent',
params: {
duration: 'PT0.5S',
request: {
method: 'handleExport',
params: ['p1'],
Expand All @@ -160,7 +219,7 @@ describe('snap_scheduleBackgroundEvent', () => {
});

expect(scheduleBackgroundEvent).toHaveBeenCalledWith({
date: expect.any(String),
date: '2025-05-21T13:25:21.500Z',
request: {
method: 'handleExport',
params: ['p1'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,21 @@ export type ScheduleBackgroundEventParameters = InferMatching<
*/
function getStartDate(params: ScheduleBackgroundEventParams) {
if ('duration' in params) {
return DateTime.fromJSDate(new Date())
.toUTC()
.plus(Duration.fromISO(params.duration));
const parsed = Duration.fromISO(params.duration);

// Disallow durations less than 1 second.
const duration =
parsed.as('seconds') >= 1 ? parsed : Duration.fromObject({ seconds: 1 });

return DateTime.fromJSDate(new Date()).toUTC().plus(duration).toISO();
}

return DateTime.fromISO(params.date, { setZone: true });
const date = DateTime.fromISO(params.date, { setZone: true });

// Make sure any millisecond precision is removed.
return date.startOf('second').toISO({
suppressMilliseconds: true,
});
}

/**
Expand Down Expand Up @@ -128,14 +137,9 @@ async function getScheduleBackgroundEventImplementation(

const date = getStartDate(validatedParams);

// Make sure any millisecond precision is removed.
const truncatedDate = date.startOf('second').toISO({
suppressMilliseconds: true,
});

assert(truncatedDate);
assert(date);

const id = scheduleBackgroundEvent({ date: truncatedDate, request });
const id = scheduleBackgroundEvent({ date, request });
res.result = id;
} catch (error) {
return end(error);
Expand Down
Loading