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
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: 94.98,
functions: 98.64,
lines: 98.76,
statements: 98.45,
branches: 94.95,
functions: 98.62,
lines: 98.75,
statements: 98.43,
},
},
});
4 changes: 1 addition & 3 deletions packages/snaps-rpc-methods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@
"@metamask/snaps-utils": "workspace:^",
"@metamask/superstruct": "^3.1.0",
"@metamask/utils": "^11.2.0",
"@noble/hashes": "^1.7.1",
"luxon": "^3.5.0"
"@noble/hashes": "^1.7.1"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^3.0.4",
Expand All @@ -72,7 +71,6 @@
"@swc/core": "1.3.78",
"@swc/jest": "^0.2.26",
"@ts-bridge/cli": "^0.6.1",
"@types/luxon": "^3",
"@types/node": "18.14.2",
"deepmerge": "^4.2.2",
"depcheck": "^1.4.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,18 @@ import {
type ScheduleBackgroundEventResult,
} from '@metamask/snaps-sdk';
import type { CronjobRpcRequest, InferMatching } from '@metamask/snaps-utils';
import { CronjobRpcRequestStruct } from '@metamask/snaps-utils';
import {
StructError,
create,
object,
refine,
string,
} from '@metamask/superstruct';
CronjobRpcRequestStruct,
getStartDate,
Iso8601DateStruct,
Iso8601DurationStruct,
} from '@metamask/snaps-utils';
import { StructError, create, object } from '@metamask/superstruct';
import {
assert,
hasProperty,
type PendingJsonRpcResponse,
} from '@metamask/utils';
import { DateTime, Duration } from 'luxon';

import { SnapEndowments } from '../endowments';
import type { MethodHooksObject } from '../utils';
Expand Down Expand Up @@ -56,31 +54,13 @@ export const scheduleBackgroundEventHandler: PermittedHandlerExport<
hookNames,
};

const offsetRegex = /Z|([+-]\d{2}:?\d{2})$/u;

const ScheduleBackgroundEventParametersWithDateStruct = object({
date: refine(string(), 'date', (val) => {
const date = DateTime.fromISO(val);
if (date.isValid) {
// Luxon doesn't have a reliable way to check if timezone info was not provided
if (!offsetRegex.test(val)) {
return 'ISO 8601 date must have timezone information';
}
return true;
}
return 'Not a valid ISO 8601 date';
}),
date: Iso8601DateStruct,
request: CronjobRpcRequestStruct,
});

const ScheduleBackgroundEventParametersWithDurationStruct = object({
duration: refine(string(), 'duration', (val) => {
const duration = Duration.fromISO(val);
if (!duration.isValid) {
return 'Not a valid ISO 8601 duration';
}
return true;
}),
duration: Iso8601DurationStruct,
request: CronjobRpcRequestStruct,
});

Expand All @@ -96,22 +76,6 @@ export type ScheduleBackgroundEventParameters = InferMatching<
ScheduleBackgroundEventParams
>;

/**
* Generates a `DateTime` object based on if a duration or date is provided.
*
* @param params - The validated params from the `snap_scheduleBackgroundEvent` call.
* @returns A `DateTime` object.
*/
function getStartDate(params: ScheduleBackgroundEventParams) {
if ('duration' in params) {
return DateTime.fromJSDate(new Date())
.toUTC()
.plus(Duration.fromISO(params.duration));
}

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

/**
* The `snap_scheduleBackgroundEvent` method implementation.
*
Expand Down Expand Up @@ -146,7 +110,11 @@ async function getScheduleBackgroundEventImplementation(

const { request } = validatedParams;

const date = getStartDate(validatedParams);
const time = hasProperty(validatedParams, 'date')
? (validatedParams.date as string)
: validatedParams.duration;

const date = getStartDate(time);

// Make sure any millisecond precision is removed.
const truncatedDate = date.startOf('second').toISO({
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-utils/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 99.74,
"functions": 98.93,
"lines": 99.61,
"statements": 96.95
"branches": 99.75,
"functions": 98.95,
"lines": 99.62,
"statements": 97
}
2 changes: 2 additions & 0 deletions packages/snaps-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"fast-deep-equal": "^3.1.3",
"fast-json-stable-stringify": "^2.1.0",
"fast-xml-parser": "^4.4.1",
"luxon": "^3.5.0",
"marked": "^12.0.1",
"rfdc": "^1.3.0",
"semver": "^7.5.4",
Expand All @@ -111,6 +112,7 @@
"@swc/jest": "^0.2.26",
"@ts-bridge/cli": "^0.6.1",
"@types/jest": "^27.5.1",
"@types/luxon": "^3",
"@types/mocha": "^10.0.1",
"@types/node": "18.14.2",
"@types/semver": "^7.5.0",
Expand Down
1 change: 1 addition & 0 deletions packages/snaps-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './platform-version';
export * from './snaps';
export * from './strings';
export * from './structs';
export * from './time';
export * from './types';
export * from './ui';
export * from './url';
Expand Down
75 changes: 75 additions & 0 deletions packages/snaps-utils/src/time.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { create, is } from '@metamask/superstruct';
import { DateTime } from 'luxon';

import { getStartDate, Iso8601DateStruct, Iso8601DurationStruct } from './time';

describe('Iso8601dateStruct', () => {
it('should return true for a valid ISO 8601 date', () => {
const value = DateTime.now().toISO();
expect(is(value, Iso8601DateStruct)).toBe(true);
});

it('should return false for an invalid ISO 8601 date', () => {
const value = 'Mon Mar 31 2025';
expect(is(value, Iso8601DateStruct)).toBe(false);
});

it('should return false for an ISO 8601 date without timezone information', () => {
const value = '2025-03-31T12:00:00';
expect(is(value, Iso8601DateStruct)).toBe(false);
});

it('should return an error message for invalid ISO 8601 date', () => {
const value = 'Mon Mar 31 2025';
expect(() => create(value, Iso8601DateStruct)).toThrow(
'Not a valid ISO 8601 date',
);
});

it('should return an error message for ISO 8601 date without timezone information', () => {
const value = '2025-03-31T12:00:00';
expect(() => create(value, Iso8601DateStruct)).toThrow(
'ISO 8601 date must have timezone information',
);
});
});

describe('Iso8601DurationStruct', () => {
it('should return true for a valid ISO 8601 duration', () => {
const value = 'P3Y6M4DT12H30M5S';
expect(is(value, Iso8601DurationStruct)).toBe(true);
});

it('should return false for an invalid ISO 8601 duration', () => {
const value = 'Millisecond';
expect(is(value, Iso8601DurationStruct)).toBe(false);
});

it('should return an error message for invalid ISO 8601 duration', () => {
const value = '1Millisecond';
expect(() => create(value, Iso8601DurationStruct)).toThrow(
'Not a valid ISO 8601 duration',
);
});
});

describe('getStartDate', () => {
it('should return a DateTime object for a valid ISO 8601 date', () => {
const value = '2025-03-31T12:00:00Z';
const final = DateTime.fromISO(value, { setZone: true });

expect(getStartDate(value)).toStrictEqual(final);
});

it('should return a DateTime object with the valid ISO 8601 duration added', () => {
jest
.useFakeTimers('modern')
.setSystemTime(new Date('2025-03-31T12:00:00Z'));

const value = 'P3Y6M';

expect(getStartDate(value)).toStrictEqual(
DateTime.fromISO('2028-09-30T12:00:00.000Z', { setZone: true }),
);
});
});
56 changes: 56 additions & 0 deletions packages/snaps-utils/src/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { is, refine, string } from '@metamask/superstruct';
import { DateTime, Duration } from 'luxon';

/**
* Refines a string as an ISO 8601 duration.
*/
export const Iso8601DurationStruct = refine(
string(),
'ISO 8601 duration',
(value) => {
const parsedDuration = Duration.fromISO(value);
if (!parsedDuration.isValid) {
return 'Not a valid ISO 8601 duration';
}
return true;
},
);

/**
* Regex to match the offset part of an ISO 8601 date.
*/
const offsetRegex = /Z|([+-]\d{2}:?\d{2})$/u;

/**
* Refines a string as an ISO 8601 date.
*/
export const Iso8601DateStruct = refine(string(), 'ISO 8601 date', (value) => {
const parsedDate = DateTime.fromISO(value);

if (!parsedDate.isValid) {
return 'Not a valid ISO 8601 date';
}

if (!offsetRegex.test(value)) {
// Luxon doesn't have a reliable way to check if timezone info was not provided
return 'ISO 8601 date must have timezone information';
}

return true;
});

/**
* Generates a `DateTime` object based on if a duration or date is provided.
*
* @param iso8601Time - The ISO 8601 time string.
* @returns A `DateTime` object.
*/
export function getStartDate(iso8601Time: string) {
if (is(iso8601Time, Iso8601DurationStruct)) {
return DateTime.fromJSDate(new Date())
.toUTC()
.plus(Duration.fromISO(iso8601Time));
}

return DateTime.fromISO(iso8601Time, { setZone: true });
}
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5845,15 +5845,13 @@ __metadata:
"@swc/core": "npm:1.3.78"
"@swc/jest": "npm:^0.2.26"
"@ts-bridge/cli": "npm:^0.6.1"
"@types/luxon": "npm:^3"
"@types/node": "npm:18.14.2"
deepmerge: "npm:^4.2.2"
depcheck: "npm:^1.4.7"
eslint: "npm:^9.11.0"
jest: "npm:^29.0.2"
jest-it-up: "npm:^2.0.0"
jest-silent-reporter: "npm:^0.6.0"
luxon: "npm:^3.5.0"
prettier: "npm:^3.3.3"
typescript: "npm:~5.3.3"
languageName: unknown
Expand Down Expand Up @@ -6049,6 +6047,7 @@ __metadata:
"@swc/jest": "npm:^0.2.26"
"@ts-bridge/cli": "npm:^0.6.1"
"@types/jest": "npm:^27.5.1"
"@types/luxon": "npm:^3"
"@types/mocha": "npm:^10.0.1"
"@types/node": "npm:18.14.2"
"@types/semver": "npm:^7.5.0"
Expand All @@ -6075,6 +6074,7 @@ __metadata:
istanbul-reports: "npm:^3.1.5"
jest: "npm:^29.0.2"
jest-silent-reporter: "npm:^0.6.0"
luxon: "npm:^3.5.0"
marked: "npm:^12.0.1"
memfs: "npm:^3.4.13"
prettier: "npm:^3.3.3"
Expand Down
Loading