From 7cd146552473ece0dbdd69d11230fe4aedd4f3c6 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Thu, 9 Oct 2025 12:19:58 +1300 Subject: [PATCH 01/10] Add new time periods and refactor related logic for ERC20 and native token permissions --- .../gator-permissions-snap/src/core/types.ts | 3 ++ .../erc20TokenPeriodic/content.tsx | 8 +--- .../permissions/erc20TokenPeriodic/context.ts | 18 ++------ .../permissions/erc20TokenPeriodic/rules.ts | 44 ++++++------------- .../permissions/erc20TokenPeriodic/types.ts | 2 +- .../nativeTokenPeriodic/content.tsx | 8 +--- .../nativeTokenPeriodic/context.ts | 22 +++------- .../permissions/nativeTokenPeriodic/rules.ts | 44 ++++++------------- .../permissions/nativeTokenPeriodic/types.ts | 2 +- .../gator-permissions-snap/src/utils/time.ts | 41 +++++++++++++++-- 10 files changed, 81 insertions(+), 111 deletions(-) diff --git a/packages/gator-permissions-snap/src/core/types.ts b/packages/gator-permissions-snap/src/core/types.ts index 2561be29..ccd0e6aa 100644 --- a/packages/gator-permissions-snap/src/core/types.ts +++ b/packages/gator-permissions-snap/src/core/types.ts @@ -91,9 +91,12 @@ export type DeepRequired = TParent extends (infer U)[] * An enum representing the time periods for which the stream rate can be calculated. */ export enum TimePeriod { + HOURLY = 'Hourly', DAILY = 'Daily', WEEKLY = 'Weekly', + BIWEEKLY = 'Biweekly', MONTHLY = 'Monthly', + YEARLY = 'Yearly', } /** diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx index 6b27d268..2c2ce607 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx @@ -4,7 +4,6 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, periodTypeRule, - periodDurationRule, startTimeRule, expiryRule, } from './rules'; @@ -32,12 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [ - startTimeRule, - periodAmountRule, - periodTypeRule, - periodDurationRule, - ], + rules: [startTimeRule, periodAmountRule, periodTypeRule], context, metadata, })} diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index 49c4bbe9..86c7bdb6 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -8,9 +8,8 @@ import { type Hex, } from '@metamask/utils'; -import { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -178,19 +177,8 @@ export async function buildContext({ decimals, }); - const periodDuration = data.periodDuration.toString(); - - // Determine the period type based on the duration - let periodType: TimePeriod | 'Other'; - if (periodDuration === TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY].toString()) { - periodType = TimePeriod.DAILY; - } else if ( - periodDuration === TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY].toString() - ) { - periodType = TimePeriod.WEEKLY; - } else { - periodType = 'Other'; - } + const periodType = getClosestTimePeriod(BigInt(data.periodDuration)); + const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts index da37e1a8..99f1aa0f 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -42,22 +42,28 @@ export const periodTypeRule: RuleDefinition< Erc20TokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Period duration', + label: 'Transfer Window', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, value: context.permissionDetails.periodType, isVisible: true, tooltip: 'The duration of the period', - options: [TimePeriod.DAILY, TimePeriod.WEEKLY, 'Other'], + options: [ + TimePeriod.HOURLY, + TimePeriod.DAILY, + TimePeriod.WEEKLY, + TimePeriod.BIWEEKLY, + TimePeriod.MONTHLY, + TimePeriod.YEARLY, + ], error: metadata.validationErrors.periodTypeError, }), updateContext: (context: Erc20TokenPeriodicContext, value: string) => { - const periodType = value as TimePeriod | 'Other'; - const periodDuration = - periodType === 'Other' - ? context.permissionDetails.periodDuration - : Number(TIME_PERIOD_TO_SECONDS[periodType]).toString(); + const periodType = value as TimePeriod; + const periodDuration = Number( + TIME_PERIOD_TO_SECONDS[periodType], + ).toString(); return { ...context, @@ -70,29 +76,6 @@ export const periodTypeRule: RuleDefinition< }, }; -export const periodDurationRule: RuleDefinition< - Erc20TokenPeriodicContext, - Erc20TokenPeriodicMetadata -> = { - name: PERIOD_DURATION_ELEMENT, - label: 'Duration (seconds)', - type: 'number', - getRuleData: ({ context, metadata }) => ({ - value: context.permissionDetails.periodDuration, - isAdjustmentAllowed: context.isAdjustmentAllowed, - isVisible: context.permissionDetails.periodType === 'Other', - tooltip: 'The length of each period in seconds', - error: metadata.validationErrors.periodDurationError, - }), - updateContext: (context: Erc20TokenPeriodicContext, value: string) => ({ - ...context, - permissionDetails: { - ...context.permissionDetails, - periodDuration: value, - }, - }), -}; - export const startTimeRule: RuleDefinition< Erc20TokenPeriodicContext, Erc20TokenPeriodicMetadata @@ -162,7 +145,6 @@ export const expiryRule: RuleDefinition< export const allRules = [ periodAmountRule, periodTypeRule, - periodDurationRule, startTimeRule, expiryRule, ]; diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts index b47ec8bf..0ebfaae6 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts @@ -29,7 +29,7 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & { export type Erc20TokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod | 'Other'; + periodType: TimePeriod; periodDuration: string; startTime: number; }; diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx index 5d12c5fa..ab9a1476 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx @@ -4,7 +4,6 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, periodTypeRule, - periodDurationRule, startTimeRule, expiryRule, } from './rules'; @@ -32,12 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [ - startTimeRule, - periodAmountRule, - periodTypeRule, - periodDurationRule, - ], + rules: [startTimeRule, periodAmountRule, periodTypeRule], context, metadata, })} diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index 31c13c03..e82e0ff3 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -8,9 +8,8 @@ import { type Hex, } from '@metamask/utils'; -import { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -178,19 +177,12 @@ export async function buildContext({ decimals, }); - const periodDuration = data.periodDuration.toString(); - - // Determine the period type based on the duration - let periodType: TimePeriod | 'Other'; - if (periodDuration === TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY].toString()) { - periodType = TimePeriod.DAILY; - } else if ( - periodDuration === TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY].toString() - ) { - periodType = TimePeriod.WEEKLY; - } else { - periodType = 'Other'; - } + console.log('zzzz', data); + + const periodType = getClosestTimePeriod(BigInt(data.periodDuration)); + const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); + + console.log('yyyyy', { periodType, periodDuration }); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts index a1072f99..d1ce835b 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -42,22 +42,28 @@ export const periodTypeRule: RuleDefinition< NativeTokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Period duration', + label: 'Transfer Window', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, value: context.permissionDetails.periodType, isVisible: true, tooltip: 'The duration of the period', - options: [TimePeriod.DAILY, TimePeriod.WEEKLY, 'Other'], + options: [ + TimePeriod.HOURLY, + TimePeriod.DAILY, + TimePeriod.WEEKLY, + TimePeriod.BIWEEKLY, + TimePeriod.MONTHLY, + TimePeriod.YEARLY, + ], error: metadata.validationErrors.periodTypeError, }), updateContext: (context: NativeTokenPeriodicContext, value: string) => { - const periodType = value as TimePeriod | 'Other'; - const periodDuration = - periodType === 'Other' - ? context.permissionDetails.periodDuration - : Number(TIME_PERIOD_TO_SECONDS[periodType]).toString(); + const periodType = value as TimePeriod; + const periodDuration = Number( + TIME_PERIOD_TO_SECONDS[periodType], + ).toString(); return { ...context, @@ -70,29 +76,6 @@ export const periodTypeRule: RuleDefinition< }, }; -export const periodDurationRule: RuleDefinition< - NativeTokenPeriodicContext, - NativeTokenPeriodicMetadata -> = { - name: PERIOD_DURATION_ELEMENT, - label: 'Duration (seconds)', - type: 'number', - getRuleData: ({ context, metadata }) => ({ - value: context.permissionDetails.periodDuration, - isAdjustmentAllowed: context.isAdjustmentAllowed, - isVisible: context.permissionDetails.periodType === 'Other', - tooltip: 'The length of each period in seconds', - error: metadata.validationErrors.periodDurationError, - }), - updateContext: (context: NativeTokenPeriodicContext, value: string) => ({ - ...context, - permissionDetails: { - ...context.permissionDetails, - periodDuration: value, - }, - }), -}; - export const startTimeRule: RuleDefinition< NativeTokenPeriodicContext, NativeTokenPeriodicMetadata @@ -162,7 +145,6 @@ export const expiryRule: RuleDefinition< export const allRules = [ periodAmountRule, periodTypeRule, - periodDurationRule, startTimeRule, expiryRule, ]; diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts index 467bcea1..cbfdee45 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts @@ -28,7 +28,7 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & { export type NativeTokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod | 'Other'; + periodType: TimePeriod; periodDuration: string; startTime: number; }; diff --git a/packages/gator-permissions-snap/src/utils/time.ts b/packages/gator-permissions-snap/src/utils/time.ts index 651b0719..4f94c423 100644 --- a/packages/gator-permissions-snap/src/utils/time.ts +++ b/packages/gator-permissions-snap/src/utils/time.ts @@ -253,9 +253,44 @@ export const getStartOfNextDayUTC = (): number => { * A mapping of time periods to their equivalent seconds. */ export const TIME_PERIOD_TO_SECONDS: Record = { + [TimePeriod.HOURLY]: 60n * 60n, // 3,600(seconds) [TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400(seconds) [TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800(seconds) - // Monthly is difficult because months are not consistent in length. - // We approximate by calculating the number of seconds in 1/12th of a year. - [TimePeriod.MONTHLY]: (60n * 60n * 24n * 365n) / 12n, // 2,629,760(seconds) + [TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600(seconds) + [TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000(seconds) + [TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000(seconds) +}; + +/** + * Finds the closest TimePeriod enum value for a given duration in seconds. + * + * @param seconds - The duration in seconds to match. + * @returns The TimePeriod that most closely matches the given duration. + * @throws InvalidInputError if no time periods are available. + */ +export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { + const timePeriodEntries = Object.entries(TIME_PERIOD_TO_SECONDS) as [ + TimePeriod, + bigint, + ][]; + + const firstEntry = timePeriodEntries[0]; + if (!firstEntry) { + throw new InvalidInputError('No time periods available'); + } + + let closestPeriod = firstEntry[0]; + let minDifference = + seconds > firstEntry[1] ? seconds - firstEntry[1] : firstEntry[1] - seconds; + + for (const [period, periodValue] of timePeriodEntries.slice(1)) { + const difference = + seconds > periodValue ? seconds - periodValue : periodValue - seconds; + if (difference < minDifference) { + minDifference = difference; + closestPeriod = period; + } + } + + return closestPeriod; }; From 254eeb18f17b258cdbee5fea437d2d8725caa39c Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Thu, 9 Oct 2025 13:06:45 +1300 Subject: [PATCH 02/10] Update tests --- .../erc20TokenPeriodic/content.test.ts | 352 +++++++++++------- .../erc20TokenStream/content.test.ts | 48 +++ .../nativeTokenPeriodic/content.test.ts | 154 +++----- .../nativeTokenStream/content.test.ts | 96 +++++ 4 files changed, 424 insertions(+), 226 deletions(-) diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts index 6d398272..88881f8a 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts @@ -305,7 +305,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -349,6 +349,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -366,10 +374,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", "props": { - "children": "Other", - "value": "Other", + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -387,7 +411,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -825,7 +848,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -869,6 +892,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -886,10 +917,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", "props": { - "children": "Other", - "value": "Other", + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -907,7 +954,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -1341,7 +1387,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -1385,6 +1431,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1402,10 +1456,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", "props": { - "children": "Other", - "value": "Other", + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -1423,7 +1493,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -1855,7 +1924,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -1899,6 +1968,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1916,10 +1993,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", "props": { - "children": "Other", - "value": "Other", + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -1937,95 +2030,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Duration (seconds)", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The length of each period in seconds", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - { - "key": null, - "props": { - "name": "erc20-token-periodic-period-duration", - "type": "number", - "value": "123456", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - ], - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, ], }, "type": "Section", @@ -2447,7 +2451,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -2491,6 +2495,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -2508,10 +2520,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", "props": { - "children": "Other", - "value": "Other", + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -2529,7 +2557,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -2952,7 +2979,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -2996,6 +3023,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -3013,10 +3048,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", "props": { - "children": "Other", - "value": "Other", + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -3034,7 +3085,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -3475,7 +3525,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -3519,6 +3569,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -3536,10 +3594,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", "props": { - "children": "Other", - "value": "Other", + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -3557,7 +3631,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -3988,7 +4061,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -4032,6 +4105,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -4049,10 +4130,26 @@ describe('erc20TokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", "props": { - "children": "Other", - "value": "Other", + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -4070,7 +4167,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenStream/content.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenStream/content.test.ts index 4dcae593..38cbd363 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenStream/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenStream/content.test.ts @@ -214,6 +214,14 @@ describe('erc20TokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -230,6 +238,14 @@ describe('erc20TokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -238,6 +254,14 @@ describe('erc20TokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "erc20-token-stream-time-period", "value": "Weekly", @@ -1069,6 +1093,14 @@ describe('erc20TokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1085,6 +1117,14 @@ describe('erc20TokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -1093,6 +1133,14 @@ describe('erc20TokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "erc20-token-stream-time-period", "value": "Weekly", diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts index 2c3f083a..653c9b00 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts @@ -294,7 +294,7 @@ describe('nativeTokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -338,6 +338,14 @@ describe('nativeTokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -355,10 +363,26 @@ describe('nativeTokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", "props": { - "children": "Other", - "value": "Other", + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -376,7 +400,6 @@ describe('nativeTokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -797,7 +820,7 @@ describe('nativeTokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Transfer Window", }, "type": "Text", }, @@ -841,6 +864,14 @@ describe('nativeTokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -858,10 +889,26 @@ describe('nativeTokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, + { + "key": "Monthly", + "props": { + "children": "Monthly", + "value": "Monthly", + }, + "type": "Option", + }, + { + "key": "Yearly", "props": { - "children": "Other", - "value": "Other", + "children": "Yearly", + "value": "Yearly", }, "type": "Option", }, @@ -879,95 +926,6 @@ describe('nativeTokenPeriodic:content', () => { }, "type": "Box", }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Duration (seconds)", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The length of each period in seconds", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - { - "key": null, - "props": { - "name": "native-token-periodic-period-duration", - "type": "number", - "value": "86400", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - ], - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, ], }, "type": "Section", diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenStream/content.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenStream/content.test.ts index b019cfcc..e474695f 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenStream/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenStream/content.test.ts @@ -202,6 +202,14 @@ describe('nativeTokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -218,6 +226,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -226,6 +242,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "native-token-stream-time-period", "value": "Weekly", @@ -1024,6 +1048,14 @@ describe('nativeTokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1040,6 +1072,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -1048,6 +1088,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "native-token-stream-time-period", "value": "Weekly", @@ -2489,6 +2537,14 @@ describe('nativeTokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -2505,6 +2561,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -2513,6 +2577,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "native-token-stream-time-period", "value": "Weekly", @@ -3244,6 +3316,14 @@ describe('nativeTokenStream:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -3260,6 +3340,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Biweekly", + "props": { + "children": "Biweekly", + "value": "Biweekly", + }, + "type": "Option", + }, { "key": "Monthly", "props": { @@ -3268,6 +3356,14 @@ describe('nativeTokenStream:content', () => { }, "type": "Option", }, + { + "key": "Yearly", + "props": { + "children": "Yearly", + "value": "Yearly", + }, + "type": "Option", + }, ], "name": "native-token-stream-time-period", "value": "Daily", From 78ec2faf7f61674ca8a3ec9ebdb3e0ef26b23e4e Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Thu, 9 Oct 2025 16:14:44 +1300 Subject: [PATCH 03/10] Add tests for getClosestTimePeriod function and related time periods --- .../test/utils/time.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/gator-permissions-snap/test/utils/time.test.ts b/packages/gator-permissions-snap/test/utils/time.test.ts index 1b3e3947..afe03719 100644 --- a/packages/gator-permissions-snap/test/utils/time.test.ts +++ b/packages/gator-permissions-snap/test/utils/time.test.ts @@ -1,8 +1,11 @@ +import { TimePeriod } from '../../src/core/types'; import { convertTimestampToReadableDate, convertReadableDateToTimestamp, getStartOfTodayUTC, getStartOfNextDayUTC, + getClosestTimePeriod, + TIME_PERIOD_TO_SECONDS, } from '../../src/utils/time'; describe('Time Utility Functions', () => { @@ -120,4 +123,83 @@ describe('Time Utility Functions', () => { expect(startOfNextDay - startOfToday).toBe(24 * 60 * 60); }); }); + + describe('getClosestTimePeriod', () => { + it('should return HOURLY for exactly 1 hour', () => { + const oneHour = TIME_PERIOD_TO_SECONDS[TimePeriod.HOURLY]; + expect(getClosestTimePeriod(oneHour)).toBe(TimePeriod.HOURLY); + }); + + it('should return DAILY for exactly 1 day', () => { + const oneDay = TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]; + expect(getClosestTimePeriod(oneDay)).toBe(TimePeriod.DAILY); + }); + + it('should return WEEKLY for exactly 1 week', () => { + const oneWeek = TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY]; + expect(getClosestTimePeriod(oneWeek)).toBe(TimePeriod.WEEKLY); + }); + + it('should return BIWEEKLY for exactly 2 weeks', () => { + const twoWeeks = TIME_PERIOD_TO_SECONDS[TimePeriod.BIWEEKLY]; + expect(getClosestTimePeriod(twoWeeks)).toBe(TimePeriod.BIWEEKLY); + }); + + it('should return MONTHLY for exactly 30 days', () => { + const oneMonth = TIME_PERIOD_TO_SECONDS[TimePeriod.MONTHLY]; + expect(getClosestTimePeriod(oneMonth)).toBe(TimePeriod.MONTHLY); + }); + + it('should return YEARLY for exactly 365 days', () => { + const oneYear = TIME_PERIOD_TO_SECONDS[TimePeriod.YEARLY]; + expect(getClosestTimePeriod(oneYear)).toBe(TimePeriod.YEARLY); + }); + + it('should return HOURLY for values closer to 1 hour', () => { + expect(getClosestTimePeriod(3000n)).toBe(TimePeriod.HOURLY); // ~50 minutes + expect(getClosestTimePeriod(4000n)).toBe(TimePeriod.HOURLY); // ~66 minutes + }); + + it('should return DAILY for values closer to 1 day', () => { + expect(getClosestTimePeriod(80000n)).toBe(TimePeriod.DAILY); // ~22 hours + expect(getClosestTimePeriod(90000n)).toBe(TimePeriod.DAILY); // ~25 hours + }); + + it('should return WEEKLY for values closer to 1 week', () => { + expect(getClosestTimePeriod(500000n)).toBe(TimePeriod.WEEKLY); // ~5.8 days + expect(getClosestTimePeriod(700000n)).toBe(TimePeriod.WEEKLY); // ~8.1 days + }); + + it('should return BIWEEKLY for values closer to 2 weeks', () => { + expect(getClosestTimePeriod(1100000n)).toBe(TimePeriod.BIWEEKLY); // ~12.7 days + expect(getClosestTimePeriod(1300000n)).toBe(TimePeriod.BIWEEKLY); // ~15 days + }); + + it('should return MONTHLY for values closer to 30 days', () => { + expect(getClosestTimePeriod(2000000n)).toBe(TimePeriod.MONTHLY); // ~23 days + expect(getClosestTimePeriod(2800000n)).toBe(TimePeriod.MONTHLY); // ~32 days + }); + + it('should return YEARLY for values closer to 365 days', () => { + expect(getClosestTimePeriod(20000000n)).toBe(TimePeriod.YEARLY); // ~231 days + expect(getClosestTimePeriod(40000000n)).toBe(TimePeriod.YEARLY); // ~463 days + }); + + it('should handle very small values', () => { + expect(getClosestTimePeriod(1n)).toBe(TimePeriod.HOURLY); // Closest to hourly + expect(getClosestTimePeriod(100n)).toBe(TimePeriod.HOURLY); + }); + + it('should handle very large values', () => { + expect(getClosestTimePeriod(50000000n)).toBe(TimePeriod.YEARLY); // ~578 days + expect(getClosestTimePeriod(100000000n)).toBe(TimePeriod.YEARLY); // ~1157 days + }); + + it('should handle boundary cases between periods', () => { + // Exactly halfway between HOURLY (3,600) and DAILY (86,400) + const halfwayHourlyDaily = (3600n + 86400n) / 2n; // 45,000 + const result = getClosestTimePeriod(halfwayHourlyDaily); + expect([TimePeriod.HOURLY, TimePeriod.DAILY]).toContain(result); + }); + }); }); From 8673a95a0b8dee8e3fd6aad85e3bde35e6342274 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Wed, 15 Oct 2025 11:18:38 +1300 Subject: [PATCH 04/10] Remove debug logs and add comments for time period constants in context and time utility files --- .../src/permissions/nativeTokenPeriodic/context.ts | 4 ---- packages/gator-permissions-snap/src/utils/time.ts | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index e82e0ff3..fdce010a 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -177,13 +177,9 @@ export async function buildContext({ decimals, }); - console.log('zzzz', data); - const periodType = getClosestTimePeriod(BigInt(data.periodDuration)); const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); - console.log('yyyyy', { periodType, periodDuration }); - const startTime = data.startTime ?? Math.floor(Date.now() / 1000); const tokenAddressCaip19 = toCaipAssetType( diff --git a/packages/gator-permissions-snap/src/utils/time.ts b/packages/gator-permissions-snap/src/utils/time.ts index 4f94c423..11e478d1 100644 --- a/packages/gator-permissions-snap/src/utils/time.ts +++ b/packages/gator-permissions-snap/src/utils/time.ts @@ -255,10 +255,10 @@ export const getStartOfNextDayUTC = (): number => { export const TIME_PERIOD_TO_SECONDS: Record = { [TimePeriod.HOURLY]: 60n * 60n, // 3,600(seconds) [TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400(seconds) - [TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800(seconds) - [TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600(seconds) - [TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000(seconds) - [TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000(seconds) + [TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800(seconds), 7 days + [TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600(seconds), 14 days + [TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000(seconds), 30 days + [TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000(seconds), 365 days }; /** From 15f94a336ef11202694b292a4162a4842da8e9b2 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Wed, 15 Oct 2025 15:42:21 +1300 Subject: [PATCH 05/10] Enhance time period validation and error handling in ERC20 and native token contexts; add tests for edge cases in getClosestTimePeriod function --- .../permissions/erc20TokenPeriodic/context.ts | 21 +- .../permissions/erc20TokenPeriodic/rules.ts | 23 +- .../nativeTokenPeriodic/context.ts | 21 +- .../permissions/nativeTokenPeriodic/rules.ts | 23 +- .../gator-permissions-snap/src/utils/time.ts | 40 +- .../erc20TokenPeriodic/content.test.ts | 537 ------------------ .../nativeTokenPeriodic/content.test.ts | 526 ----------------- .../test/utils/time.test.ts | 25 + 8 files changed, 134 insertions(+), 1082 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index 86c7bdb6..94dc2d9a 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -177,7 +177,26 @@ export async function buildContext({ decimals, }); - const periodType = getClosestTimePeriod(BigInt(data.periodDuration)); + // Safely convert period duration to BigInt with error handling + let periodDurationBigInt: bigint; + try { + periodDurationBigInt = BigInt(data.periodDuration); + } catch (error) { + throw new InvalidInputError( + `Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`, + ); + } + + // Validate that the duration is positive + if (periodDurationBigInt <= 0n) { + throw new InvalidInputError( + `Period duration must be positive. Received: ${periodDurationBigInt} seconds.`, + ); + } + + // Map the requested duration to the closest standard time period. + // This normalizes non-standard durations to predefined periods. + const periodType = getClosestTimePeriod(periodDurationBigInt); const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts index 99f1aa0f..125b5ad9 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -1,3 +1,5 @@ +import { InvalidInputError } from '@metamask/snaps-sdk'; + import { TimePeriod } from '../../core/types'; import type { RuleDefinition } from '../../core/types'; import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; @@ -9,7 +11,6 @@ import type { export const PERIOD_AMOUNT_ELEMENT = 'erc20-token-periodic-period-amount'; export const PERIOD_TYPE_ELEMENT = 'erc20-token-periodic-period-type'; -export const PERIOD_DURATION_ELEMENT = 'erc20-token-periodic-period-duration'; export const START_TIME_ELEMENT = 'erc20-token-periodic-start-date'; export const EXPIRY_ELEMENT = 'erc20-token-periodic-expiry'; @@ -60,10 +61,24 @@ export const periodTypeRule: RuleDefinition< error: metadata.validationErrors.periodTypeError, }), updateContext: (context: Erc20TokenPeriodicContext, value: string) => { + // Validate that value is a valid TimePeriod + if (!Object.values(TimePeriod).includes(value as TimePeriod)) { + throw new InvalidInputError( + `Invalid period type: "${value}". Valid options are: ${Object.values(TimePeriod).join(', ')}`, + ); + } + const periodType = value as TimePeriod; - const periodDuration = Number( - TIME_PERIOD_TO_SECONDS[periodType], - ).toString(); + const periodSeconds = TIME_PERIOD_TO_SECONDS[periodType]; + + // This should never happen if the above check passed, but be defensive + if (periodSeconds === undefined) { + throw new InvalidInputError( + `Period type "${periodType}" is not mapped to a duration. This indicates a system error.`, + ); + } + + const periodDuration = Number(periodSeconds).toString(); return { ...context, diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index fdce010a..5c832f09 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -177,7 +177,26 @@ export async function buildContext({ decimals, }); - const periodType = getClosestTimePeriod(BigInt(data.periodDuration)); + // Safely convert period duration to BigInt with error handling + let periodDurationBigInt: bigint; + try { + periodDurationBigInt = BigInt(data.periodDuration); + } catch (error) { + throw new InvalidInputError( + `Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`, + ); + } + + // Validate that the duration is positive + if (periodDurationBigInt <= 0n) { + throw new InvalidInputError( + `Period duration must be positive. Received: ${periodDurationBigInt} seconds.`, + ); + } + + // Map the requested duration to the closest standard time period. + // This normalizes non-standard durations to predefined periods. + const periodType = getClosestTimePeriod(periodDurationBigInt); const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts index d1ce835b..a14a3e45 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -1,3 +1,5 @@ +import { InvalidInputError } from '@metamask/snaps-sdk'; + import { TimePeriod } from '../../core/types'; import type { RuleDefinition } from '../../core/types'; import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; @@ -9,7 +11,6 @@ import type { export const PERIOD_AMOUNT_ELEMENT = 'native-token-periodic-period-amount'; export const PERIOD_TYPE_ELEMENT = 'native-token-periodic-period-type'; -export const PERIOD_DURATION_ELEMENT = 'native-token-periodic-period-duration'; export const START_TIME_ELEMENT = 'native-token-periodic-start-date'; export const EXPIRY_ELEMENT = 'native-token-periodic-expiry'; @@ -60,10 +61,24 @@ export const periodTypeRule: RuleDefinition< error: metadata.validationErrors.periodTypeError, }), updateContext: (context: NativeTokenPeriodicContext, value: string) => { + // Validate that value is a valid TimePeriod + if (!Object.values(TimePeriod).includes(value as TimePeriod)) { + throw new InvalidInputError( + `Invalid period type: "${value}". Valid options are: ${Object.values(TimePeriod).join(', ')}`, + ); + } + const periodType = value as TimePeriod; - const periodDuration = Number( - TIME_PERIOD_TO_SECONDS[periodType], - ).toString(); + const periodSeconds = TIME_PERIOD_TO_SECONDS[periodType]; + + // This should never happen if the above check passed, but be defensive + if (periodSeconds === undefined) { + throw new InvalidInputError( + `Period type "${periodType}" is not mapped to a duration. This indicates a system error.`, + ); + } + + const periodDuration = Number(periodSeconds).toString(); return { ...context, diff --git a/packages/gator-permissions-snap/src/utils/time.ts b/packages/gator-permissions-snap/src/utils/time.ts index 11e478d1..c5520373 100644 --- a/packages/gator-permissions-snap/src/utils/time.ts +++ b/packages/gator-permissions-snap/src/utils/time.ts @@ -253,22 +253,42 @@ export const getStartOfNextDayUTC = (): number => { * A mapping of time periods to their equivalent seconds. */ export const TIME_PERIOD_TO_SECONDS: Record = { - [TimePeriod.HOURLY]: 60n * 60n, // 3,600(seconds) - [TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400(seconds) - [TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800(seconds), 7 days - [TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600(seconds), 14 days - [TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000(seconds), 30 days - [TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000(seconds), 365 days + [TimePeriod.HOURLY]: 60n * 60n, // 3,600 seconds (1 hour) + [TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400 seconds (1 day) + [TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800 seconds (7 days) + [TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600 seconds (14 days) + [TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000 seconds (approximated as 30 days, real months vary 28-31 days) + [TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000 seconds (365 days, does not account for leap years) }; /** * Finds the closest TimePeriod enum value for a given duration in seconds. + * Uses absolute difference to find the nearest match by comparing against all + * predefined time periods (HOURLY, DAILY, WEEKLY, BIWEEKLY, MONTHLY, YEARLY). * - * @param seconds - The duration in seconds to match. + * @param seconds - The duration in seconds to match. Must be positive and reasonable. * @returns The TimePeriod that most closely matches the given duration. - * @throws InvalidInputError if no time periods are available. + * @throws InvalidInputError if seconds is invalid or no time periods are available. + * @example + * getClosestTimePeriod(80000n) // Returns TimePeriod.DAILY (~22 hours) + * getClosestTimePeriod(1300000n) // Returns TimePeriod.BIWEEKLY (~15 days) */ export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { + // Validate input range + if (seconds <= 0n) { + throw new InvalidInputError( + `Period duration must be positive. Received: ${seconds} seconds.`, + ); + } + + // Warn about absurdly large values (more than 10 years) + const TEN_YEARS = 60n * 60n * 24n * 365n * 10n; + if (seconds > TEN_YEARS) { + throw new InvalidInputError( + `Period duration ${seconds} seconds (${seconds / (60n * 60n * 24n)} days) is too large. Maximum supported period is 10 years.`, + ); + } + const timePeriodEntries = Object.entries(TIME_PERIOD_TO_SECONDS) as [ TimePeriod, bigint, @@ -276,7 +296,9 @@ export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { const firstEntry = timePeriodEntries[0]; if (!firstEntry) { - throw new InvalidInputError('No time periods available'); + throw new InvalidInputError( + `No time periods available. This indicates a system error. Input: ${seconds} seconds, Available periods: ${Object.keys(TIME_PERIOD_TO_SECONDS).length}`, + ); } let closestPeriod = firstEntry[0]; diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts index 88881f8a..dfc43b77 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts @@ -1649,543 +1649,6 @@ describe('erc20TokenPeriodic:content', () => { `); }); - it('should render content with custom period duration', async () => { - const customContext = { - ...mockContext, - permissionDetails: { - ...mockContext.permissionDetails, - periodType: 'Other' as const, - periodDuration: '123456', - }, - }; - - const content = await createConfirmationContent({ - context: customContext, - metadata: mockMetadata, - }); - - expect(content).toMatchInlineSnapshot(` -{ - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Start Time", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The time at which the first period begins(mm/dd/yyyy hh:mm:ss).", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "end", - "children": { - "key": null, - "props": { - "children": "mm/dd/yyyy hh:mm:ss", - "color": "muted", - "size": "sm", - }, - "type": "Text", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "space-between", - "children": { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "name": "erc20-token-periodic-start-date_date", - "placeholder": "mm/dd/yyyy", - "type": "text", - "value": "10/26/1985", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "name": "erc20-token-periodic-start-date_time", - "placeholder": "HH:MM:SS", - "type": "text", - "value": "08:00:00", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "alignment": "center", - "children": { - "key": null, - "props": { - "alignment": "center", - "children": "UTC", - }, - "type": "Text", - }, - "direction": "vertical", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - null, - null, - ], - }, - "type": "Box", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Amount", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The amount of tokens granted during each period", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "alt": "USDC", - "src": " - - ", - }, - "type": "Image", - }, - }, - "type": "Box", - }, - { - "key": null, - "props": { - "name": "erc20-token-periodic-period-amount", - "type": "number", - "value": "100", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - ], - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Transfer Window", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The duration of the period", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "children": [ - { - "key": "Hourly", - "props": { - "children": "Hourly", - "value": "Hourly", - }, - "type": "Option", - }, - { - "key": "Daily", - "props": { - "children": "Daily", - "value": "Daily", - }, - "type": "Option", - }, - { - "key": "Weekly", - "props": { - "children": "Weekly", - "value": "Weekly", - }, - "type": "Option", - }, - { - "key": "Biweekly", - "props": { - "children": "Biweekly", - "value": "Biweekly", - }, - "type": "Option", - }, - { - "key": "Monthly", - "props": { - "children": "Monthly", - "value": "Monthly", - }, - "type": "Option", - }, - { - "key": "Yearly", - "props": { - "children": "Yearly", - "value": "Yearly", - }, - "type": "Option", - }, - ], - "name": "erc20-token-periodic-period-type", - "value": "Other", - }, - "type": "Dropdown", - }, - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - ], - }, - "type": "Section", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Expiry", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The expiry date of the permission(mm/dd/yyyy hh:mm:ss).", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "end", - "children": { - "key": null, - "props": { - "children": "mm/dd/yyyy hh:mm:ss", - "color": "muted", - "size": "sm", - }, - "type": "Text", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "space-between", - "children": { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "name": "erc20-token-periodic-expiry_date", - "placeholder": "mm/dd/yyyy", - "type": "text", - "value": "05/01/2024", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "name": "erc20-token-periodic-expiry_time", - "placeholder": "HH:MM:SS", - "type": "text", - "value": "00:00:00", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "alignment": "center", - "children": { - "key": null, - "props": { - "alignment": "center", - "children": "UTC", - }, - "type": "Text", - }, - "direction": "vertical", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - null, - null, - ], - }, - "type": "Box", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - ], - }, - "type": "Section", - }, - ], - }, - "type": "Box", -} -`); - }); - it('should render content without token icon', async () => { const contextWithoutIcon = { ...mockContext, diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts index 653c9b00..c810b654 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts @@ -553,532 +553,6 @@ describe('nativeTokenPeriodic:content', () => { }, "type": "Box", } -`); - }); - - it('should render the period duration if the period type is "Other"', async () => { - const context: NativeTokenPeriodicContext = { - ...mockContext, - permissionDetails: { - ...mockContext.permissionDetails, - periodType: 'Other', - }, - }; - const content = await createConfirmationContent({ - context, - metadata: mockMetadata, - }); - - expect(content).toMatchInlineSnapshot(` -{ - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Start Time", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The time at which the first period begins(mm/dd/yyyy hh:mm:ss).", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "end", - "children": { - "key": null, - "props": { - "children": "mm/dd/yyyy hh:mm:ss", - "color": "muted", - "size": "sm", - }, - "type": "Text", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "space-between", - "children": { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "name": "native-token-periodic-start-date_date", - "placeholder": "mm/dd/yyyy", - "type": "text", - "value": "10/26/1985", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "name": "native-token-periodic-start-date_time", - "placeholder": "HH:MM:SS", - "type": "text", - "value": "08:00:00", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "alignment": "center", - "children": { - "key": null, - "props": { - "alignment": "center", - "children": "UTC", - }, - "type": "Text", - }, - "direction": "vertical", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - null, - null, - ], - }, - "type": "Box", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Amount", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The amount of tokens granted during each period", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - { - "key": null, - "props": { - "name": "native-token-periodic-period-amount", - "type": "number", - "value": "1", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - ], - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Transfer Window", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The duration of the period", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "children": [ - { - "key": "Hourly", - "props": { - "children": "Hourly", - "value": "Hourly", - }, - "type": "Option", - }, - { - "key": "Daily", - "props": { - "children": "Daily", - "value": "Daily", - }, - "type": "Option", - }, - { - "key": "Weekly", - "props": { - "children": "Weekly", - "value": "Weekly", - }, - "type": "Option", - }, - { - "key": "Biweekly", - "props": { - "children": "Biweekly", - "value": "Biweekly", - }, - "type": "Option", - }, - { - "key": "Monthly", - "props": { - "children": "Monthly", - "value": "Monthly", - }, - "type": "Option", - }, - { - "key": "Yearly", - "props": { - "children": "Yearly", - "value": "Yearly", - }, - "type": "Option", - }, - ], - "name": "native-token-periodic-period-type", - "value": "Other", - }, - "type": "Dropdown", - }, - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - ], - }, - "type": "Section", - }, - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "alignment": "space-between", - "children": [ - { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "children": "Expiry", - }, - "type": "Text", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "color": "muted", - "name": "question", - "size": "inherit", - }, - "type": "Icon", - }, - "content": { - "key": null, - "props": { - "children": "The expiry date of the permission(mm/dd/yyyy hh:mm:ss).", - }, - "type": "Text", - }, - }, - "type": "Tooltip", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "end", - "children": { - "key": null, - "props": { - "children": "mm/dd/yyyy hh:mm:ss", - "color": "muted", - "size": "sm", - }, - "type": "Text", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "alignment": "space-between", - "children": { - "key": null, - "props": { - "children": [ - { - "key": null, - "props": { - "name": "native-token-periodic-expiry_date", - "placeholder": "mm/dd/yyyy", - "type": "text", - "value": "05/01/2024", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "name": "native-token-periodic-expiry_time", - "placeholder": "HH:MM:SS", - "type": "text", - "value": "00:00:00", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "alignment": "center", - "children": { - "key": null, - "props": { - "alignment": "center", - "children": "UTC", - }, - "type": "Text", - }, - "direction": "vertical", - }, - "type": "Box", - }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - null, - null, - ], - }, - "type": "Box", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - ], - }, - "type": "Section", - }, - ], - }, - "type": "Box", -} `); }); }); diff --git a/packages/gator-permissions-snap/test/utils/time.test.ts b/packages/gator-permissions-snap/test/utils/time.test.ts index afe03719..74b5f7b1 100644 --- a/packages/gator-permissions-snap/test/utils/time.test.ts +++ b/packages/gator-permissions-snap/test/utils/time.test.ts @@ -201,5 +201,30 @@ describe('Time Utility Functions', () => { const result = getClosestTimePeriod(halfwayHourlyDaily); expect([TimePeriod.HOURLY, TimePeriod.DAILY]).toContain(result); }); + + it('should throw error for zero duration', () => { + expect(() => getClosestTimePeriod(0n)).toThrow( + 'Period duration must be positive. Received: 0 seconds.', + ); + }); + + it('should throw error for negative duration', () => { + expect(() => getClosestTimePeriod(-86400n)).toThrow( + 'Period duration must be positive. Received: -86400 seconds.', + ); + }); + + it('should throw error for absurdly large values (> 10 years)', () => { + const elevenYears = 60n * 60n * 24n * 365n * 11n; + expect(() => getClosestTimePeriod(elevenYears)).toThrow( + 'is too large. Maximum supported period is 10 years.', + ); + }); + + it('should accept values up to 10 years', () => { + const tenYears = 60n * 60n * 24n * 365n * 10n; + expect(() => getClosestTimePeriod(tenYears)).not.toThrow(); + expect(getClosestTimePeriod(tenYears)).toBe(TimePeriod.YEARLY); + }); }); }); From a51061f82dd2225392f5057c4fe16ec4305e5fbe Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Fri, 17 Oct 2025 18:22:31 +1300 Subject: [PATCH 06/10] Refactor periodDuration type to use zPeriodDuration in ERC20 and native token contexts; add zPeriodDuration definition --- .../src/permissions/erc20TokenPeriodic/types.ts | 4 ++-- .../src/permissions/nativeTokenPeriodic/types.ts | 4 ++-- packages/gator-permissions-snap/src/utils/time.ts | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts index 0ebfaae6..90955487 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts @@ -3,7 +3,6 @@ import { zPermission, zMetaMaskPermissionData, zAddress, - zTimestamp, zStartTime, } from '@metamask/7715-permissions-shared/types'; import { z } from 'zod'; @@ -15,6 +14,7 @@ import type { TimePeriod, BaseMetadata, } from '../../core/types'; +import { zPeriodDuration } from '../../utils/time'; export type Erc20TokenPeriodicMetadata = BaseMetadata & { validationErrors: { @@ -41,7 +41,7 @@ export const zErc20TokenPeriodicPermission = zPermission.extend({ zMetaMaskPermissionData, z.object({ periodAmount: zHexStr, - periodDuration: zTimestamp, + periodDuration: zPeriodDuration, startTime: zStartTime, tokenAddress: zAddress, }), diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts index cbfdee45..2dfa1012 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts @@ -2,7 +2,6 @@ import { zHexStr, zPermission, zMetaMaskPermissionData, - zTimestamp, zStartTime, } from '@metamask/7715-permissions-shared/types'; import { z } from 'zod'; @@ -14,6 +13,7 @@ import type { TimePeriod, BaseMetadata, } from '../../core/types'; +import { zPeriodDuration } from '../../utils/time'; export type NativeTokenPeriodicMetadata = BaseMetadata & { validationErrors: { @@ -40,7 +40,7 @@ export const zNativeTokenPeriodicPermission = zPermission.extend({ zMetaMaskPermissionData, z.object({ periodAmount: zHexStr, - periodDuration: zTimestamp, + periodDuration: zPeriodDuration, startTime: zStartTime, }), ), diff --git a/packages/gator-permissions-snap/src/utils/time.ts b/packages/gator-permissions-snap/src/utils/time.ts index c5520373..a1377152 100644 --- a/packages/gator-permissions-snap/src/utils/time.ts +++ b/packages/gator-permissions-snap/src/utils/time.ts @@ -1,3 +1,4 @@ +import { zTimestamp } from '@metamask/7715-permissions-shared/types'; import { InvalidInputError } from '@metamask/snaps-sdk'; import { TimePeriod } from '../core/types'; @@ -316,3 +317,10 @@ export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { return closestPeriod; }; + +/** + * period duration in seconds, mapped to closest TransferWindow enum value + */ +export const zPeriodDuration = zTimestamp.transform((val) => { + return val; // getClosestTimePeriod(BigInt(val)) as unknown as number; +}); From a85fcfcbb966faabb595ccde1ee6bed0c2a28cac Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Tue, 21 Oct 2025 14:39:23 +1300 Subject: [PATCH 07/10] Refactor time period handling to simplify duration conversion; update tests and labels for consistency --- .../permissions/erc20TokenPeriodic/context.ts | 21 +----- .../permissions/erc20TokenPeriodic/rules.ts | 2 +- .../nativeTokenPeriodic/context.ts | 21 +----- .../permissions/nativeTokenPeriodic/rules.ts | 2 +- .../gator-permissions-snap/src/utils/time.ts | 50 +++++--------- .../erc20TokenPeriodic/content.test.ts | 14 ++-- .../nativeTokenPeriodic/content.test.ts | 2 +- .../test/utils/time.test.ts | 67 +++++++------------ 8 files changed, 52 insertions(+), 127 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index 94dc2d9a..fe66e9c9 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -177,26 +177,7 @@ export async function buildContext({ decimals, }); - // Safely convert period duration to BigInt with error handling - let periodDurationBigInt: bigint; - try { - periodDurationBigInt = BigInt(data.periodDuration); - } catch (error) { - throw new InvalidInputError( - `Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`, - ); - } - - // Validate that the duration is positive - if (periodDurationBigInt <= 0n) { - throw new InvalidInputError( - `Period duration must be positive. Received: ${periodDurationBigInt} seconds.`, - ); - } - - // Map the requested duration to the closest standard time period. - // This normalizes non-standard durations to predefined periods. - const periodType = getClosestTimePeriod(periodDurationBigInt); + const periodType = getClosestTimePeriod(data.periodDuration); const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts index 125b5ad9..7505ed73 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -43,7 +43,7 @@ export const periodTypeRule: RuleDefinition< Erc20TokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Transfer Window', + label: 'Frequency', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index 5c832f09..81c87da0 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -177,26 +177,7 @@ export async function buildContext({ decimals, }); - // Safely convert period duration to BigInt with error handling - let periodDurationBigInt: bigint; - try { - periodDurationBigInt = BigInt(data.periodDuration); - } catch (error) { - throw new InvalidInputError( - `Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`, - ); - } - - // Validate that the duration is positive - if (periodDurationBigInt <= 0n) { - throw new InvalidInputError( - `Period duration must be positive. Received: ${periodDurationBigInt} seconds.`, - ); - } - - // Map the requested duration to the closest standard time period. - // This normalizes non-standard durations to predefined periods. - const periodType = getClosestTimePeriod(periodDurationBigInt); + const periodType = getClosestTimePeriod(data.periodDuration); const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts index a14a3e45..ef5c7a16 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -43,7 +43,7 @@ export const periodTypeRule: RuleDefinition< NativeTokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Transfer Window', + label: 'Frequency', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, diff --git a/packages/gator-permissions-snap/src/utils/time.ts b/packages/gator-permissions-snap/src/utils/time.ts index a1377152..f90c4e28 100644 --- a/packages/gator-permissions-snap/src/utils/time.ts +++ b/packages/gator-permissions-snap/src/utils/time.ts @@ -269,46 +269,22 @@ export const TIME_PERIOD_TO_SECONDS: Record = { * * @param seconds - The duration in seconds to match. Must be positive and reasonable. * @returns The TimePeriod that most closely matches the given duration. - * @throws InvalidInputError if seconds is invalid or no time periods are available. * @example - * getClosestTimePeriod(80000n) // Returns TimePeriod.DAILY (~22 hours) - * getClosestTimePeriod(1300000n) // Returns TimePeriod.BIWEEKLY (~15 days) + * getClosestTimePeriod(80000) // Returns TimePeriod.DAILY (~22 hours) + * getClosestTimePeriod(1300000) // Returns TimePeriod.BIWEEKLY (~15 days) */ -export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { - // Validate input range - if (seconds <= 0n) { - throw new InvalidInputError( - `Period duration must be positive. Received: ${seconds} seconds.`, - ); - } - - // Warn about absurdly large values (more than 10 years) - const TEN_YEARS = 60n * 60n * 24n * 365n * 10n; - if (seconds > TEN_YEARS) { - throw new InvalidInputError( - `Period duration ${seconds} seconds (${seconds / (60n * 60n * 24n)} days) is too large. Maximum supported period is 10 years.`, - ); - } - +export const getClosestTimePeriod = (seconds: number): TimePeriod => { const timePeriodEntries = Object.entries(TIME_PERIOD_TO_SECONDS) as [ TimePeriod, bigint, ][]; - const firstEntry = timePeriodEntries[0]; - if (!firstEntry) { - throw new InvalidInputError( - `No time periods available. This indicates a system error. Input: ${seconds} seconds, Available periods: ${Object.keys(TIME_PERIOD_TO_SECONDS).length}`, - ); - } + let closestPeriod = TimePeriod.HOURLY; + let minDifference = Number.MAX_SAFE_INTEGER; - let closestPeriod = firstEntry[0]; - let minDifference = - seconds > firstEntry[1] ? seconds - firstEntry[1] : firstEntry[1] - seconds; + for (const [period, periodValue] of timePeriodEntries) { + const difference = Math.abs(seconds - Number(periodValue)); - for (const [period, periodValue] of timePeriodEntries.slice(1)) { - const difference = - seconds > periodValue ? seconds - periodValue : periodValue - seconds; if (difference < minDifference) { minDifference = difference; closestPeriod = period; @@ -318,9 +294,15 @@ export const getClosestTimePeriod = (seconds: bigint): TimePeriod => { return closestPeriod; }; +const TEN_YEARS = 10 * 365 * 24 * 60 * 60; // 10 years in seconds /** * period duration in seconds, mapped to closest TransferWindow enum value */ -export const zPeriodDuration = zTimestamp.transform((val) => { - return val; // getClosestTimePeriod(BigInt(val)) as unknown as number; -}); +export const zPeriodDuration = zTimestamp + .max(TEN_YEARS, { + message: `Period duration must be less than or equal to ${TEN_YEARS} seconds (10 years).`, + }) + .transform((val) => { + const periodType = getClosestTimePeriod(val); + return Number(TIME_PERIOD_TO_SECONDS[periodType]); + }); diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts index dfc43b77..68b219af 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts @@ -305,7 +305,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -848,7 +848,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -1387,7 +1387,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -1914,7 +1914,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -2442,7 +2442,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -2988,7 +2988,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, @@ -3524,7 +3524,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts index c810b654..75231de6 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts @@ -294,7 +294,7 @@ describe('nativeTokenPeriodic:content', () => { { "key": null, "props": { - "children": "Transfer Window", + "children": "Frequency", }, "type": "Text", }, diff --git a/packages/gator-permissions-snap/test/utils/time.test.ts b/packages/gator-permissions-snap/test/utils/time.test.ts index 74b5f7b1..8535d66b 100644 --- a/packages/gator-permissions-snap/test/utils/time.test.ts +++ b/packages/gator-permissions-snap/test/utils/time.test.ts @@ -126,103 +126,84 @@ describe('Time Utility Functions', () => { describe('getClosestTimePeriod', () => { it('should return HOURLY for exactly 1 hour', () => { - const oneHour = TIME_PERIOD_TO_SECONDS[TimePeriod.HOURLY]; + const oneHour = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.HOURLY]); expect(getClosestTimePeriod(oneHour)).toBe(TimePeriod.HOURLY); }); it('should return DAILY for exactly 1 day', () => { - const oneDay = TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]; + const oneDay = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]); expect(getClosestTimePeriod(oneDay)).toBe(TimePeriod.DAILY); }); it('should return WEEKLY for exactly 1 week', () => { - const oneWeek = TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY]; + const oneWeek = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY]); expect(getClosestTimePeriod(oneWeek)).toBe(TimePeriod.WEEKLY); }); it('should return BIWEEKLY for exactly 2 weeks', () => { - const twoWeeks = TIME_PERIOD_TO_SECONDS[TimePeriod.BIWEEKLY]; + const twoWeeks = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.BIWEEKLY]); expect(getClosestTimePeriod(twoWeeks)).toBe(TimePeriod.BIWEEKLY); }); it('should return MONTHLY for exactly 30 days', () => { - const oneMonth = TIME_PERIOD_TO_SECONDS[TimePeriod.MONTHLY]; + const oneMonth = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.MONTHLY]); expect(getClosestTimePeriod(oneMonth)).toBe(TimePeriod.MONTHLY); }); it('should return YEARLY for exactly 365 days', () => { - const oneYear = TIME_PERIOD_TO_SECONDS[TimePeriod.YEARLY]; + const oneYear = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.YEARLY]); expect(getClosestTimePeriod(oneYear)).toBe(TimePeriod.YEARLY); }); it('should return HOURLY for values closer to 1 hour', () => { - expect(getClosestTimePeriod(3000n)).toBe(TimePeriod.HOURLY); // ~50 minutes - expect(getClosestTimePeriod(4000n)).toBe(TimePeriod.HOURLY); // ~66 minutes + expect(getClosestTimePeriod(3000)).toBe(TimePeriod.HOURLY); // ~50 minutes + expect(getClosestTimePeriod(4000)).toBe(TimePeriod.HOURLY); // ~66 minutes }); it('should return DAILY for values closer to 1 day', () => { - expect(getClosestTimePeriod(80000n)).toBe(TimePeriod.DAILY); // ~22 hours - expect(getClosestTimePeriod(90000n)).toBe(TimePeriod.DAILY); // ~25 hours + expect(getClosestTimePeriod(80000)).toBe(TimePeriod.DAILY); // ~22 hours + expect(getClosestTimePeriod(90000)).toBe(TimePeriod.DAILY); // ~25 hours }); it('should return WEEKLY for values closer to 1 week', () => { - expect(getClosestTimePeriod(500000n)).toBe(TimePeriod.WEEKLY); // ~5.8 days - expect(getClosestTimePeriod(700000n)).toBe(TimePeriod.WEEKLY); // ~8.1 days + expect(getClosestTimePeriod(500000)).toBe(TimePeriod.WEEKLY); // ~5.8 days + expect(getClosestTimePeriod(700000)).toBe(TimePeriod.WEEKLY); // ~8.1 days }); it('should return BIWEEKLY for values closer to 2 weeks', () => { - expect(getClosestTimePeriod(1100000n)).toBe(TimePeriod.BIWEEKLY); // ~12.7 days - expect(getClosestTimePeriod(1300000n)).toBe(TimePeriod.BIWEEKLY); // ~15 days + expect(getClosestTimePeriod(1100000)).toBe(TimePeriod.BIWEEKLY); // ~12.7 days + expect(getClosestTimePeriod(1300000)).toBe(TimePeriod.BIWEEKLY); // ~15 days }); it('should return MONTHLY for values closer to 30 days', () => { - expect(getClosestTimePeriod(2000000n)).toBe(TimePeriod.MONTHLY); // ~23 days - expect(getClosestTimePeriod(2800000n)).toBe(TimePeriod.MONTHLY); // ~32 days + expect(getClosestTimePeriod(2000000)).toBe(TimePeriod.MONTHLY); // ~23 days + expect(getClosestTimePeriod(2800000)).toBe(TimePeriod.MONTHLY); // ~32 days }); it('should return YEARLY for values closer to 365 days', () => { - expect(getClosestTimePeriod(20000000n)).toBe(TimePeriod.YEARLY); // ~231 days - expect(getClosestTimePeriod(40000000n)).toBe(TimePeriod.YEARLY); // ~463 days + expect(getClosestTimePeriod(20000000)).toBe(TimePeriod.YEARLY); // ~231 days + expect(getClosestTimePeriod(40000000)).toBe(TimePeriod.YEARLY); // ~463 days }); it('should handle very small values', () => { - expect(getClosestTimePeriod(1n)).toBe(TimePeriod.HOURLY); // Closest to hourly - expect(getClosestTimePeriod(100n)).toBe(TimePeriod.HOURLY); + expect(getClosestTimePeriod(1)).toBe(TimePeriod.HOURLY); // Closest to hourly + expect(getClosestTimePeriod(100)).toBe(TimePeriod.HOURLY); }); it('should handle very large values', () => { - expect(getClosestTimePeriod(50000000n)).toBe(TimePeriod.YEARLY); // ~578 days - expect(getClosestTimePeriod(100000000n)).toBe(TimePeriod.YEARLY); // ~1157 days + expect(getClosestTimePeriod(50000000)).toBe(TimePeriod.YEARLY); // ~578 days + expect(getClosestTimePeriod(100000000)).toBe(TimePeriod.YEARLY); // ~1157 days }); it('should handle boundary cases between periods', () => { // Exactly halfway between HOURLY (3,600) and DAILY (86,400) - const halfwayHourlyDaily = (3600n + 86400n) / 2n; // 45,000 + const halfwayHourlyDaily = (3600 + 86400) / 2; // 45,000 const result = getClosestTimePeriod(halfwayHourlyDaily); expect([TimePeriod.HOURLY, TimePeriod.DAILY]).toContain(result); }); - it('should throw error for zero duration', () => { - expect(() => getClosestTimePeriod(0n)).toThrow( - 'Period duration must be positive. Received: 0 seconds.', - ); - }); - - it('should throw error for negative duration', () => { - expect(() => getClosestTimePeriod(-86400n)).toThrow( - 'Period duration must be positive. Received: -86400 seconds.', - ); - }); - - it('should throw error for absurdly large values (> 10 years)', () => { - const elevenYears = 60n * 60n * 24n * 365n * 11n; - expect(() => getClosestTimePeriod(elevenYears)).toThrow( - 'is too large. Maximum supported period is 10 years.', - ); - }); - it('should accept values up to 10 years', () => { - const tenYears = 60n * 60n * 24n * 365n * 10n; + const tenYears = 60 * 60 * 24 * 365 * 10; expect(() => getClosestTimePeriod(tenYears)).not.toThrow(); expect(getClosestTimePeriod(tenYears)).toBe(TimePeriod.YEARLY); }); From 1234de00b86bb7d572c59f6d284a22a1ab9f480b Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Tue, 21 Oct 2025 15:39:02 +1300 Subject: [PATCH 08/10] Refactor time period handling in ERC20 and native token contexts to remove getClosestTimePeriod function; simplify duration conversion logic --- .../src/permissions/erc20TokenPeriodic/context.ts | 10 +++++++--- .../src/permissions/nativeTokenPeriodic/context.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index fe66e9c9..06863a23 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -8,8 +8,9 @@ import { type Hex, } from '@metamask/utils'; +import type { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -177,8 +178,11 @@ export async function buildContext({ decimals, }); - const periodType = getClosestTimePeriod(data.periodDuration); - const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); + const periodType = (Object.keys(TIME_PERIOD_TO_SECONDS) as TimePeriod[]).find( + (key: TimePeriod) => + Number(TIME_PERIOD_TO_SECONDS[key]) === data.periodDuration, + ); + const periodDuration = data.periodDuration.toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index 81c87da0..f73be410 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -8,8 +8,9 @@ import { type Hex, } from '@metamask/utils'; +import type { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -177,8 +178,11 @@ export async function buildContext({ decimals, }); - const periodType = getClosestTimePeriod(data.periodDuration); - const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString(); + const periodType = (Object.keys(TIME_PERIOD_TO_SECONDS) as TimePeriod[]).find( + (key: TimePeriod) => + Number(TIME_PERIOD_TO_SECONDS[key]) === data.periodDuration, + ); + const periodDuration = data.periodDuration.toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); From 390adaf794633a3fd163bf65483b214b435444b7 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Wed, 22 Oct 2025 15:38:23 +1300 Subject: [PATCH 09/10] Refactor period handling to replace periodType with periodDuration in ERC20 and native token contexts; update related tests for consistency --- .../erc20TokenPeriodic/content.tsx | 4 ++-- .../permissions/erc20TokenPeriodic/context.ts | 7 ------ .../permissions/erc20TokenPeriodic/rules.ts | 22 +++++++------------ .../permissions/erc20TokenPeriodic/types.ts | 3 --- .../nativeTokenPeriodic/content.tsx | 4 ++-- .../nativeTokenPeriodic/context.ts | 7 ------ .../permissions/nativeTokenPeriodic/rules.ts | 22 +++++++------------ .../permissions/nativeTokenPeriodic/types.ts | 3 --- .../erc20TokenPeriodic/content.test.ts | 3 +-- .../erc20TokenPeriodic/context.test.ts | 3 +-- .../nativeTokenPeriodic/content.test.ts | 1 - .../nativeTokenPeriodic/context.test.ts | 3 +-- 12 files changed, 23 insertions(+), 59 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx index 2c2ce607..b1841a6e 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx @@ -3,7 +3,7 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, - periodTypeRule, + periodDurationRule, startTimeRule, expiryRule, } from './rules'; @@ -31,7 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [startTimeRule, periodAmountRule, periodTypeRule], + rules: [startTimeRule, periodAmountRule, periodDurationRule], context, metadata, })} diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index 06863a23..d3023915 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -8,9 +8,7 @@ import { type Hex, } from '@metamask/utils'; -import type { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -178,10 +176,6 @@ export async function buildContext({ decimals, }); - const periodType = (Object.keys(TIME_PERIOD_TO_SECONDS) as TimePeriod[]).find( - (key: TimePeriod) => - Number(TIME_PERIOD_TO_SECONDS[key]) === data.periodDuration, - ); const periodDuration = data.periodDuration.toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); @@ -212,7 +206,6 @@ export async function buildContext({ }, permissionDetails: { periodAmount, - periodType, periodDuration, startTime, }, diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts index 7505ed73..10b158b6 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -2,7 +2,7 @@ import { InvalidInputError } from '@metamask/snaps-sdk'; import { TimePeriod } from '../../core/types'; import type { RuleDefinition } from '../../core/types'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { getIconData } from '../iconUtil'; import type { Erc20TokenPeriodicContext, @@ -38,7 +38,7 @@ export const periodAmountRule: RuleDefinition< }), }; -export const periodTypeRule: RuleDefinition< +export const periodDurationRule: RuleDefinition< Erc20TokenPeriodicContext, Erc20TokenPeriodicMetadata > = { @@ -47,18 +47,13 @@ export const periodTypeRule: RuleDefinition< type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: context.permissionDetails.periodType, + value: getClosestTimePeriod( + parseInt(context.permissionDetails.periodDuration, 10), + ), isVisible: true, tooltip: 'The duration of the period', - options: [ - TimePeriod.HOURLY, - TimePeriod.DAILY, - TimePeriod.WEEKLY, - TimePeriod.BIWEEKLY, - TimePeriod.MONTHLY, - TimePeriod.YEARLY, - ], - error: metadata.validationErrors.periodTypeError, + options: Object.values(TimePeriod), + error: metadata.validationErrors.periodDurationError, }), updateContext: (context: Erc20TokenPeriodicContext, value: string) => { // Validate that value is a valid TimePeriod @@ -84,7 +79,6 @@ export const periodTypeRule: RuleDefinition< ...context, permissionDetails: { ...context.permissionDetails, - periodType, periodDuration, }, }; @@ -159,7 +153,7 @@ export const expiryRule: RuleDefinition< export const allRules = [ periodAmountRule, - periodTypeRule, + periodDurationRule, startTimeRule, expiryRule, ]; diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts index 90955487..9c2e0d65 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts @@ -11,7 +11,6 @@ import type { DeepRequired, TypedPermissionRequest, BaseContext, - TimePeriod, BaseMetadata, } from '../../core/types'; import { zPeriodDuration } from '../../utils/time'; @@ -20,7 +19,6 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & { validationErrors: { periodAmountError?: string; periodDurationError?: string; - periodTypeError?: string; startTimeError?: string; expiryError?: string; }; @@ -29,7 +27,6 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & { export type Erc20TokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod; periodDuration: string; startTime: number; }; diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx index ab9a1476..138f87b0 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx @@ -3,7 +3,7 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, - periodTypeRule, + periodDurationRule, startTimeRule, expiryRule, } from './rules'; @@ -31,7 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [startTimeRule, periodAmountRule, periodTypeRule], + rules: [startTimeRule, periodAmountRule, periodDurationRule], context, metadata, })} diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index f73be410..b6b496fc 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -8,9 +8,7 @@ import { type Hex, } from '@metamask/utils'; -import type { TimePeriod } from '../../core/types'; import type { TokenMetadataService } from '../../services/tokenMetadataService'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { parseUnits, formatUnitsFromHex } from '../../utils/value'; import { validateAndParseAmount, @@ -178,10 +176,6 @@ export async function buildContext({ decimals, }); - const periodType = (Object.keys(TIME_PERIOD_TO_SECONDS) as TimePeriod[]).find( - (key: TimePeriod) => - Number(TIME_PERIOD_TO_SECONDS[key]) === data.periodDuration, - ); const periodDuration = data.periodDuration.toString(); const startTime = data.startTime ?? Math.floor(Date.now() / 1000); @@ -212,7 +206,6 @@ export async function buildContext({ }, permissionDetails: { periodAmount, - periodType, periodDuration, startTime, }, diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts index ef5c7a16..65e3eb47 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -2,7 +2,7 @@ import { InvalidInputError } from '@metamask/snaps-sdk'; import { TimePeriod } from '../../core/types'; import type { RuleDefinition } from '../../core/types'; -import { TIME_PERIOD_TO_SECONDS } from '../../utils/time'; +import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time'; import { getIconData } from '../iconUtil'; import type { NativeTokenPeriodicContext, @@ -38,7 +38,7 @@ export const periodAmountRule: RuleDefinition< }), }; -export const periodTypeRule: RuleDefinition< +export const periodDurationRule: RuleDefinition< NativeTokenPeriodicContext, NativeTokenPeriodicMetadata > = { @@ -47,18 +47,13 @@ export const periodTypeRule: RuleDefinition< type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: context.permissionDetails.periodType, + value: getClosestTimePeriod( + parseInt(context.permissionDetails.periodDuration, 10), + ), isVisible: true, tooltip: 'The duration of the period', - options: [ - TimePeriod.HOURLY, - TimePeriod.DAILY, - TimePeriod.WEEKLY, - TimePeriod.BIWEEKLY, - TimePeriod.MONTHLY, - TimePeriod.YEARLY, - ], - error: metadata.validationErrors.periodTypeError, + options: Object.values(TimePeriod), + error: metadata.validationErrors.periodDurationError, }), updateContext: (context: NativeTokenPeriodicContext, value: string) => { // Validate that value is a valid TimePeriod @@ -84,7 +79,6 @@ export const periodTypeRule: RuleDefinition< ...context, permissionDetails: { ...context.permissionDetails, - periodType, periodDuration, }, }; @@ -159,7 +153,7 @@ export const expiryRule: RuleDefinition< export const allRules = [ periodAmountRule, - periodTypeRule, + periodDurationRule, startTimeRule, expiryRule, ]; diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts index 2dfa1012..36aa53c8 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts @@ -10,7 +10,6 @@ import type { DeepRequired, TypedPermissionRequest, BaseContext, - TimePeriod, BaseMetadata, } from '../../core/types'; import { zPeriodDuration } from '../../utils/time'; @@ -19,7 +18,6 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & { validationErrors: { periodAmountError?: string; periodDurationError?: string; - periodTypeError?: string; startTimeError?: string; expiryError?: string; }; @@ -28,7 +26,6 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & { export type NativeTokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod; periodDuration: string; startTime: number; }; diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts index 68b219af..502c95a0 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/content.test.ts @@ -27,7 +27,6 @@ const mockContext: Erc20TokenPeriodicContext = { }, permissionDetails: { periodAmount: '100', - periodType: TimePeriod.DAILY, periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), startTime: 499161600, // 10/26/1985 }, @@ -1115,7 +1114,6 @@ describe('erc20TokenPeriodic:content', () => { ...mockContext, permissionDetails: { ...mockContext.permissionDetails, - periodType: TimePeriod.WEEKLY, periodDuration: Number( TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY], ).toString(), @@ -3086,6 +3084,7 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Dropdown", }, + "error": "Invalid period duration", }, "type": "Field", }, diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts index a59ce270..a4acffd5 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts @@ -89,7 +89,6 @@ const alreadyPopulatedContext: Erc20TokenPeriodicContext = { }, permissionDetails: { periodAmount: '100', - periodType: TimePeriod.DAILY, periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), startTime: 1729900800, }, @@ -220,7 +219,7 @@ describe('erc20TokenPeriodic:context', () => { it('throws an error if the permission request has no rules', async () => { const permissionRequest = { ...alreadyPopulatedPermissionRequest, - rules: undefined, + rules: [], }; await expect( diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts index 75231de6..1bdd56cd 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/content.test.ts @@ -25,7 +25,6 @@ const mockContext: NativeTokenPeriodicContext = { }, permissionDetails: { periodAmount: '1', - periodType: TimePeriod.DAILY, periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), startTime: 499161600, // 10/26/1985 }, diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts index 7a530476..13ec3bfd 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts @@ -84,7 +84,6 @@ const alreadyPopulatedContext: NativeTokenPeriodicContext = { }, permissionDetails: { periodAmount: '1', - periodType: TimePeriod.DAILY, periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), startTime: 1729900800, }, @@ -210,7 +209,7 @@ describe('nativeTokenPeriodic:context', () => { it('throws an error if the permission request has no rules', async () => { const permissionRequest = { ...alreadyPopulatedPermissionRequest, - rules: undefined, + rules: [], }; await expect( From b9704d5f4e9c61e7273bc7f8ccb8422902f42ae7 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Wed, 22 Oct 2025 18:17:28 +1300 Subject: [PATCH 10/10] Refactor periodDuration handling to change type from string to number across ERC20 and native token contexts; update related tests for consistency --- .../src/permissions/contextValidation.ts | 4 ++-- .../src/permissions/erc20TokenPeriodic/context.ts | 4 ++-- .../src/permissions/erc20TokenPeriodic/rules.ts | 6 ++---- .../src/permissions/erc20TokenPeriodic/types.ts | 2 +- .../src/permissions/nativeTokenPeriodic/context.ts | 4 ++-- .../src/permissions/nativeTokenPeriodic/rules.ts | 6 ++---- .../src/permissions/nativeTokenPeriodic/types.ts | 2 +- .../test/permissions/erc20TokenPeriodic/context.test.ts | 8 ++++---- .../test/permissions/nativeTokenPeriodic/context.test.ts | 6 +++--- .../src/components/permissions/ERC20TokenPeriodicForm.tsx | 5 ++++- .../components/permissions/NativeTokenPeriodicForm.tsx | 7 +++++-- 11 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/gator-permissions-snap/src/permissions/contextValidation.ts b/packages/gator-permissions-snap/src/permissions/contextValidation.ts index a857df47..575712ba 100644 --- a/packages/gator-permissions-snap/src/permissions/contextValidation.ts +++ b/packages/gator-permissions-snap/src/permissions/contextValidation.ts @@ -157,12 +157,12 @@ export function calculateAmountPerSecond( * @param periodDuration - The period duration string to validate. * @returns Object containing parsed duration and any validation error. */ -export function validatePeriodDuration(periodDuration: string): { +export function validatePeriodDuration(periodDuration: number): { duration: number | undefined; error: string | undefined; } { try { - const duration = parseInt(periodDuration, 10); + const duration = periodDuration; if (isNaN(duration) || duration <= 0) { return { duration: undefined, diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts index d3023915..cb221e9f 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts @@ -74,7 +74,7 @@ export async function applyContext({ periodAmount: bigIntToHex( parseUnits({ formatted: permissionDetails.periodAmount, decimals }), ), - periodDuration: parseInt(permissionDetails.periodDuration, 10), + periodDuration: permissionDetails.periodDuration, startTime: permissionDetails.startTime, justification: originalRequest.permission.data.justification, tokenAddress: originalRequest.permission.data.tokenAddress, @@ -176,7 +176,7 @@ export async function buildContext({ decimals, }); - const periodDuration = data.periodDuration.toString(); + const { periodDuration } = data; const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts index 10b158b6..7cce4fbe 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -47,9 +47,7 @@ export const periodDurationRule: RuleDefinition< type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: getClosestTimePeriod( - parseInt(context.permissionDetails.periodDuration, 10), - ), + value: getClosestTimePeriod(context.permissionDetails.periodDuration), isVisible: true, tooltip: 'The duration of the period', options: Object.values(TimePeriod), @@ -73,7 +71,7 @@ export const periodDurationRule: RuleDefinition< ); } - const periodDuration = Number(periodSeconds).toString(); + const periodDuration = Number(periodSeconds); return { ...context, diff --git a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts index 9c2e0d65..39327ba2 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/types.ts @@ -27,7 +27,7 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & { export type Erc20TokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodDuration: string; + periodDuration: number; startTime: number; }; }; diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts index b6b496fc..fe375885 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts @@ -75,7 +75,7 @@ export async function applyContext({ periodAmount: bigIntToHex( parseUnits({ formatted: permissionDetails.periodAmount, decimals }), ), - periodDuration: parseInt(permissionDetails.periodDuration, 10), + periodDuration: permissionDetails.periodDuration, startTime: permissionDetails.startTime, justification: originalRequest.permission.data.justification, }; @@ -176,7 +176,7 @@ export async function buildContext({ decimals, }); - const periodDuration = data.periodDuration.toString(); + const { periodDuration } = data; const startTime = data.startTime ?? Math.floor(Date.now() / 1000); diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts index 65e3eb47..8fb06262 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -47,9 +47,7 @@ export const periodDurationRule: RuleDefinition< type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: getClosestTimePeriod( - parseInt(context.permissionDetails.periodDuration, 10), - ), + value: getClosestTimePeriod(context.permissionDetails.periodDuration), isVisible: true, tooltip: 'The duration of the period', options: Object.values(TimePeriod), @@ -73,7 +71,7 @@ export const periodDurationRule: RuleDefinition< ); } - const periodDuration = Number(periodSeconds).toString(); + const periodDuration = Number(periodSeconds); return { ...context, diff --git a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts index 36aa53c8..98b99848 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/types.ts @@ -26,7 +26,7 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & { export type NativeTokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodDuration: string; + periodDuration: number; startTime: number; }; }; diff --git a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts index a4acffd5..e41aaca0 100644 --- a/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts +++ b/packages/gator-permissions-snap/test/permissions/erc20TokenPeriodic/context.test.ts @@ -89,7 +89,7 @@ const alreadyPopulatedContext: Erc20TokenPeriodicContext = { }, permissionDetails: { periodAmount: '100', - periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), + periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]), startTime: 1729900800, }, } as const; @@ -306,7 +306,7 @@ describe('erc20TokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: 'invalid', + periodDuration: 'invalid' as unknown as number, }, }; @@ -324,7 +324,7 @@ describe('erc20TokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: '-1', + periodDuration: '-1' as unknown as number, }, }; @@ -452,7 +452,7 @@ describe('erc20TokenPeriodic:context', () => { permissionDetails: { ...alreadyPopulatedContext.permissionDetails, periodAmount: '200', - periodDuration: '604800', // 1 week + periodDuration: 604800, // 1 week startTime: Math.floor(Date.now() / 1000), }, expiry: { diff --git a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts index 13ec3bfd..6c239c75 100644 --- a/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts +++ b/packages/gator-permissions-snap/test/permissions/nativeTokenPeriodic/context.test.ts @@ -84,7 +84,7 @@ const alreadyPopulatedContext: NativeTokenPeriodicContext = { }, permissionDetails: { periodAmount: '1', - periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), + periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]), startTime: 1729900800, }, } as const; @@ -296,7 +296,7 @@ describe('nativeTokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: 'invalid', + periodDuration: 'invalid' as unknown as number, }, }; @@ -314,7 +314,7 @@ describe('nativeTokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: '-1', + periodDuration: '-1' as unknown as number, }, }; diff --git a/packages/site/src/components/permissions/ERC20TokenPeriodicForm.tsx b/packages/site/src/components/permissions/ERC20TokenPeriodicForm.tsx index c14a52ef..da2b5fdb 100644 --- a/packages/site/src/components/permissions/ERC20TokenPeriodicForm.tsx +++ b/packages/site/src/components/permissions/ERC20TokenPeriodicForm.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { parseUnits, toHex, type Hex } from 'viem'; + import type { ERC20TokenPeriodicPermissionRequest } from './types'; type ERC20TokenPeriodicFormProps = { @@ -15,7 +16,9 @@ export const ERC20TokenPeriodicForm = ({ BigInt(toHex(parseUnits('1', decimals))), ); const [periodDuration, setPeriodDuration] = useState(2592000); // 30 days in seconds - const [startTime, setStartTime] = useState(Math.floor(Date.now() / 1000)); + const [startTime, setStartTime] = useState( + Math.floor(Date.now() / 1000), + ); const [expiry, setExpiry] = useState( Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days from now ); diff --git a/packages/site/src/components/permissions/NativeTokenPeriodicForm.tsx b/packages/site/src/components/permissions/NativeTokenPeriodicForm.tsx index 97e668c4..e5153345 100644 --- a/packages/site/src/components/permissions/NativeTokenPeriodicForm.tsx +++ b/packages/site/src/components/permissions/NativeTokenPeriodicForm.tsx @@ -1,7 +1,8 @@ +import { bigIntToHex } from '@metamask/utils'; import { useCallback, useEffect, useState } from 'react'; import { parseUnits } from 'viem'; + import type { NativeTokenPeriodicPermissionRequest } from './types'; -import { bigIntToHex } from '@metamask/utils'; type NativeTokenPeriodicFormProps = { onChange: (request: NativeTokenPeriodicPermissionRequest) => void; @@ -14,7 +15,9 @@ export const NativeTokenPeriodicForm = ({ BigInt(bigIntToHex(parseUnits('1', 18))), ); const [periodDuration, setPeriodDuration] = useState(2592000); // 30 days in seconds - const [startTime, setStartTime] = useState(Math.floor(Date.now() / 1000)); + const [startTime, setStartTime] = useState( + Math.floor(Date.now() / 1000), + ); const [expiry, setExpiry] = useState( Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days from now );