diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 906d4228db..944600fe0d 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 94.98, - functions: 98.64, - lines: 98.76, - statements: 98.45, + branches: 94.93, + functions: 98.63, + lines: 98.75, + statements: 98.44, }, }, }); diff --git a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts index 6f96e8ac1b..7073708c46 100644 --- a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts +++ b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts @@ -8,14 +8,12 @@ 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, + ISO8601DateStruct, + ISO8601DurationStruct, +} from '@metamask/snaps-utils'; +import { StructError, create, object } from '@metamask/superstruct'; import { assert, hasProperty, @@ -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, }); diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index 7a20a59bf4..dcf2e2b964 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,6 +1,6 @@ { - "branches": 99.74, - "functions": 98.93, - "lines": 99.61, - "statements": 96.95 + "branches": 99.75, + "functions": 98.94, + "lines": 99.62, + "statements": 96.99 } diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 5668ca9450..5c4f33b9a9 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -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", @@ -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", diff --git a/packages/snaps-utils/src/index.ts b/packages/snaps-utils/src/index.ts index 92ae8f3843..bc35f64f4d 100644 --- a/packages/snaps-utils/src/index.ts +++ b/packages/snaps-utils/src/index.ts @@ -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'; diff --git a/packages/snaps-utils/src/time.test.ts b/packages/snaps-utils/src/time.test.ts new file mode 100644 index 0000000000..a0d65bf4fb --- /dev/null +++ b/packages/snaps-utils/src/time.test.ts @@ -0,0 +1,54 @@ +import { create, is } from '@metamask/superstruct'; +import { DateTime } from 'luxon'; + +import { 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', + ); + }); +}); diff --git a/packages/snaps-utils/src/time.ts b/packages/snaps-utils/src/time.ts new file mode 100644 index 0000000000..71a0130ead --- /dev/null +++ b/packages/snaps-utils/src/time.ts @@ -0,0 +1,40 @@ +import { 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; +}); diff --git a/yarn.lock b/yarn.lock index 381f8a7bc8..0957e665d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6049,6 +6049,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" @@ -6075,6 +6076,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"