Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 16 additions & 7 deletions packages/snaps-controllers/src/cronjob/CronjobController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
parseCronExpression,
logError,
logWarning,
toCensoredISO8601String,
} 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 +332,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 +377,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 +415,13 @@ 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: toCensoredISO8601String(event.date),
}));
}

/**
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 @@ -12,6 +12,7 @@ import {
CronjobRpcRequestStruct,
ISO8601DateStruct,
ISO8601DurationStruct,
toCensoredISO8601String,
} from '@metamask/snaps-utils';
import { StructError, create, object } from '@metamask/superstruct';
import {
Expand Down Expand Up @@ -84,12 +85,17 @@ 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 });
// Make sure any millisecond precision is removed.
return toCensoredISO8601String(params.date);
}

/**
Expand Down Expand Up @@ -128,14 +134,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
20 changes: 19 additions & 1 deletion packages/snaps-utils/src/time.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { create, is } from '@metamask/superstruct';
import { DateTime } from 'luxon';

import { ISO8601DateStruct, ISO8601DurationStruct } from './time';
import {
ISO8601DateStruct,
ISO8601DurationStruct,
toCensoredISO8601String,
} from './time';

describe('ISO8601DateStruct', () => {
it('should return true for a valid ISO 8601 date', () => {
Expand Down Expand Up @@ -52,3 +56,17 @@ describe('ISO8601DurationStruct', () => {
);
});
});

describe('toCensoredISO8601String', () => {
it('returns ISO dates as-is with no millisecond precision', () => {
expect(toCensoredISO8601String('2025-05-21T13:25:25Z')).toBe(
'2025-05-21T13:25:25Z',
);
});

it('removes millisecond precision', () => {
expect(toCensoredISO8601String('2025-05-21T13:25:21.500Z')).toBe(
'2025-05-21T13:25:21Z',
);
});
});
15 changes: 15 additions & 0 deletions packages/snaps-utils/src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,18 @@ export const ISO8601DateStruct = refine(string(), 'ISO 8601 date', (value) => {

return true;
});

/**
* Remove millisecond precision from an ISO 8601 string.
*
* @param value - A valid ISO 8601 date.
* @returns A valid ISO 8601 date with millisecond precision removed.
*/
export function toCensoredISO8601String(value: string) {
const date = DateTime.fromISO(value, { setZone: true });

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