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/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/content.tsx b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/content.tsx index 6b27d268..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,6 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, - periodTypeRule, periodDurationRule, startTimeRule, expiryRule, @@ -32,12 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [ - startTimeRule, - periodAmountRule, - periodTypeRule, - periodDurationRule, - ], + 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 49c4bbe9..cb221e9f 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 { 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, @@ -76,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, @@ -178,19 +176,7 @@ 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 { periodDuration } = data; const startTime = data.startTime ?? Math.floor(Date.now() / 1000); @@ -220,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 da37e1a8..7cce4fbe 100644 --- a/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts @@ -1,6 +1,8 @@ +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, @@ -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'; @@ -37,62 +38,51 @@ export const periodAmountRule: RuleDefinition< }), }; -export const periodTypeRule: RuleDefinition< +export const periodDurationRule: RuleDefinition< Erc20TokenPeriodicContext, Erc20TokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Period duration', + label: 'Frequency', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: context.permissionDetails.periodType, + value: getClosestTimePeriod(context.permissionDetails.periodDuration), isVisible: true, tooltip: 'The duration of the period', - options: [TimePeriod.DAILY, TimePeriod.WEEKLY, 'Other'], - error: metadata.validationErrors.periodTypeError, + options: Object.values(TimePeriod), + error: metadata.validationErrors.periodDurationError, }), 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(); + // 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 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); return { ...context, permissionDetails: { ...context.permissionDetails, - periodType, periodDuration, }, }; }, }; -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 @@ -161,7 +151,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..39327ba2 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'; @@ -12,15 +11,14 @@ import type { DeepRequired, TypedPermissionRequest, BaseContext, - TimePeriod, BaseMetadata, } from '../../core/types'; +import { zPeriodDuration } from '../../utils/time'; export type Erc20TokenPeriodicMetadata = BaseMetadata & { validationErrors: { periodAmountError?: string; periodDurationError?: string; - periodTypeError?: string; startTimeError?: string; expiryError?: string; }; @@ -29,8 +27,7 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & { export type Erc20TokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod | 'Other'; - periodDuration: string; + periodDuration: number; startTime: number; }; }; @@ -41,7 +38,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/content.tsx b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/content.tsx index 5d12c5fa..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,6 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx'; import { periodAmountRule, - periodTypeRule, periodDurationRule, startTimeRule, expiryRule, @@ -32,12 +31,7 @@ export async function createConfirmationContent({
{renderRules({ - rules: [ - startTimeRule, - periodAmountRule, - periodTypeRule, - periodDurationRule, - ], + 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 31c13c03..fe375885 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 { 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, @@ -77,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, }; @@ -178,19 +176,7 @@ 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 { periodDuration } = data; const startTime = data.startTime ?? Math.floor(Date.now() / 1000); @@ -220,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 a1072f99..8fb06262 100644 --- a/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts +++ b/packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts @@ -1,6 +1,8 @@ +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, @@ -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'; @@ -37,62 +38,51 @@ export const periodAmountRule: RuleDefinition< }), }; -export const periodTypeRule: RuleDefinition< +export const periodDurationRule: RuleDefinition< NativeTokenPeriodicContext, NativeTokenPeriodicMetadata > = { name: PERIOD_TYPE_ELEMENT, - label: 'Period duration', + label: 'Frequency', type: 'dropdown', getRuleData: ({ context, metadata }) => ({ isAdjustmentAllowed: context.isAdjustmentAllowed, - value: context.permissionDetails.periodType, + value: getClosestTimePeriod(context.permissionDetails.periodDuration), isVisible: true, tooltip: 'The duration of the period', - options: [TimePeriod.DAILY, TimePeriod.WEEKLY, 'Other'], - error: metadata.validationErrors.periodTypeError, + options: Object.values(TimePeriod), + error: metadata.validationErrors.periodDurationError, }), 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(); + // 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 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); return { ...context, permissionDetails: { ...context.permissionDetails, - periodType, periodDuration, }, }; }, }; -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 @@ -161,7 +151,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..98b99848 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'; @@ -11,15 +10,14 @@ import type { DeepRequired, TypedPermissionRequest, BaseContext, - TimePeriod, BaseMetadata, } from '../../core/types'; +import { zPeriodDuration } from '../../utils/time'; export type NativeTokenPeriodicMetadata = BaseMetadata & { validationErrors: { periodAmountError?: string; periodDurationError?: string; - periodTypeError?: string; startTimeError?: string; expiryError?: string; }; @@ -28,8 +26,7 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & { export type NativeTokenPeriodicContext = BaseContext & { permissionDetails: { periodAmount: string; - periodType: TimePeriod | 'Other'; - periodDuration: string; + periodDuration: number; startTime: number; }; }; @@ -40,7 +37,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 651b0719..f90c4e28 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'; @@ -253,9 +254,55 @@ export const getStartOfNextDayUTC = (): number => { * A mapping of time periods to their equivalent seconds. */ export const TIME_PERIOD_TO_SECONDS: Record = { - [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.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. Must be positive and reasonable. + * @returns The TimePeriod that most closely matches the given duration. + * @example + * getClosestTimePeriod(80000) // Returns TimePeriod.DAILY (~22 hours) + * getClosestTimePeriod(1300000) // Returns TimePeriod.BIWEEKLY (~15 days) + */ +export const getClosestTimePeriod = (seconds: number): TimePeriod => { + const timePeriodEntries = Object.entries(TIME_PERIOD_TO_SECONDS) as [ + TimePeriod, + bigint, + ][]; + + let closestPeriod = TimePeriod.HOURLY; + let minDifference = Number.MAX_SAFE_INTEGER; + + for (const [period, periodValue] of timePeriodEntries) { + const difference = Math.abs(seconds - Number(periodValue)); + + if (difference < minDifference) { + minDifference = difference; + closestPeriod = period; + } + } + + 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 + .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 6d398272..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 }, @@ -305,7 +304,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -350,525 +349,13 @@ describe('erc20TokenPeriodic:content', () => { "props": { "children": [ { - "key": "Daily", - "props": { - "children": "Daily", - "value": "Daily", - }, - "type": "Option", - }, - { - "key": "Weekly", - "props": { - "children": "Weekly", - "value": "Weekly", - }, - "type": "Option", - }, - { - "key": "Other", - "props": { - "children": "Other", - "value": "Other", - }, - "type": "Option", - }, - ], - "name": "erc20-token-periodic-period-type", - "value": "Daily", - }, - "type": "Dropdown", - }, - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - null, - ], - }, - "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 with validation errors', async () => { - const contextWithErrors = { - ...mockContext, - permissionDetails: { - ...mockContext.permissionDetails, - periodAmount: 'invalid', - }, - }; - - const metadataWithErrors: Erc20TokenPeriodicMetadata = { - validationErrors: { - periodAmountError: 'Invalid Period amount', - }, - }; - - const content = await createConfirmationContent({ - context: contextWithErrors, - metadata: metadataWithErrors, - }); - - 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": "invalid", - }, - "type": "Input", - }, - { - "key": null, - "props": { - "children": null, - }, - "type": "Box", - }, - ], - "error": "Invalid Period amount", - }, - "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": "Period duration", - }, - "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", + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", }, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - null, - ], - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": { - "key": null, - "props": { - "children": [ + "type": "Option", + }, { "key": "Daily", "props": { @@ -886,10 +373,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 +410,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -1064,21 +566,24 @@ describe('erc20TokenPeriodic:content', () => { `); }); - it('should render content with weekly period', async () => { - const weeklyContext = { + it('should render content with validation errors', async () => { + const contextWithErrors = { ...mockContext, permissionDetails: { ...mockContext.permissionDetails, - periodType: TimePeriod.WEEKLY, - periodDuration: Number( - TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY], - ).toString(), + periodAmount: 'invalid', + }, + }; + + const metadataWithErrors: Erc20TokenPeriodicMetadata = { + validationErrors: { + periodAmountError: 'Invalid Period amount', }, }; const content = await createConfirmationContent({ - context: weeklyContext, - metadata: mockMetadata, + context: contextWithErrors, + metadata: metadataWithErrors, }); expect(content).toMatchInlineSnapshot(` @@ -1305,7 +810,7 @@ describe('erc20TokenPeriodic:content', () => { "props": { "name": "erc20-token-periodic-period-amount", "type": "number", - "value": "100", + "value": "invalid", }, "type": "Input", }, @@ -1317,6 +822,7 @@ describe('erc20TokenPeriodic:content', () => { "type": "Box", }, ], + "error": "Invalid Period amount", }, "type": "Field", }, @@ -1341,7 +847,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -1385,6 +891,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1402,16 +916,32 @@ 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", }, ], "name": "erc20-token-periodic-period-type", - "value": "Weekly", + "value": "Daily", }, "type": "Dropdown", }, @@ -1423,7 +953,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -1580,18 +1109,19 @@ describe('erc20TokenPeriodic:content', () => { `); }); - it('should render content with custom period duration', async () => { - const customContext = { + it('should render content with weekly period', async () => { + const weeklyContext = { ...mockContext, permissionDetails: { ...mockContext.permissionDetails, - periodType: 'Other' as const, - periodDuration: '123456', + periodDuration: Number( + TIME_PERIOD_TO_SECONDS[TimePeriod.WEEKLY], + ).toString(), }, }; const content = await createConfirmationContent({ - context: customContext, + context: weeklyContext, metadata: mockMetadata, }); @@ -1855,7 +1385,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -1899,6 +1429,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -1916,16 +1454,32 @@ 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", }, ], "name": "erc20-token-periodic-period-type", - "value": "Other", + "value": "Weekly", }, "type": "Dropdown", }, @@ -1937,95 +1491,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 +1912,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -2491,6 +1956,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -2508,10 +1981,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", }, @@ -2529,7 +2018,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -2952,7 +2440,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -2996,6 +2484,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -3013,10 +2509,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", }, @@ -3034,7 +2546,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -3475,7 +2986,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -3519,6 +3030,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -3536,10 +3055,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", }, @@ -3549,6 +3084,7 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Dropdown", }, + "error": "Invalid period duration", }, "type": "Field", }, @@ -3557,7 +3093,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", @@ -3988,7 +3523,7 @@ describe('erc20TokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -4032,6 +3567,14 @@ describe('erc20TokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -4049,10 +3592,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 +3629,6 @@ describe('erc20TokenPeriodic:content', () => { }, "type": "Box", }, - null, ], }, "type": "Section", 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..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,8 +89,7 @@ const alreadyPopulatedContext: Erc20TokenPeriodicContext = { }, permissionDetails: { periodAmount: '100', - periodType: TimePeriod.DAILY, - periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), + periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]), startTime: 1729900800, }, } as const; @@ -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( @@ -307,7 +306,7 @@ describe('erc20TokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: 'invalid', + periodDuration: 'invalid' as unknown as number, }, }; @@ -325,7 +324,7 @@ describe('erc20TokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: '-1', + periodDuration: '-1' as unknown as number, }, }; @@ -453,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/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..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 }, @@ -294,7 +293,7 @@ describe('nativeTokenPeriodic:content', () => { { "key": null, "props": { - "children": "Period duration", + "children": "Frequency", }, "type": "Text", }, @@ -338,6 +337,14 @@ describe('nativeTokenPeriodic:content', () => { "key": null, "props": { "children": [ + { + "key": "Hourly", + "props": { + "children": "Hourly", + "value": "Hourly", + }, + "type": "Option", + }, { "key": "Daily", "props": { @@ -355,611 +362,35 @@ describe('nativeTokenPeriodic:content', () => { "type": "Option", }, { - "key": "Other", + "key": "Biweekly", "props": { - "children": "Other", - "value": "Other", + "children": "Biweekly", + "value": "Biweekly", }, "type": "Option", }, - ], - "name": "native-token-periodic-period-type", - "value": "Daily", - }, - "type": "Dropdown", - }, - }, - "type": "Field", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - null, - ], - }, - "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, + "key": "Monthly", "props": { - "name": "native-token-periodic-expiry_time", - "placeholder": "HH:MM:SS", - "type": "text", - "value": "00:00:00", + "children": "Monthly", + "value": "Monthly", }, - "type": "Input", + "type": "Option", }, { - "key": null, + "key": "Yearly", "props": { - "alignment": "center", - "children": { - "key": null, - "props": { - "alignment": "center", - "children": "UTC", - }, - "type": "Text", - }, - "direction": "vertical", + "children": "Yearly", + "value": "Yearly", }, - "type": "Box", + "type": "Option", }, ], - "direction": "horizontal", + "name": "native-token-periodic-period-type", + "value": "Daily", }, - "type": "Box", + "type": "Dropdown", }, - "direction": "horizontal", - }, - "type": "Box", - }, - { - "key": null, - "props": { - "children": [ - null, - null, - ], - }, - "type": "Box", - }, - ], - "direction": "vertical", - }, - "type": "Box", - }, - ], - }, - "type": "Section", - }, - ], - }, - "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": "Period duration", - }, - "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": "Daily", - "props": { - "children": "Daily", - "value": "Daily", - }, - "type": "Option", - }, - { - "key": "Weekly", - "props": { - "children": "Weekly", - "value": "Weekly", - }, - "type": "Option", - }, - { - "key": "Other", - "props": { - "children": "Other", - "value": "Other", - }, - "type": "Option", - }, - ], - "name": "native-token-periodic-period-type", - "value": "Other", - }, - "type": "Dropdown", - }, - }, - "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": "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", }, 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..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,8 +84,7 @@ const alreadyPopulatedContext: NativeTokenPeriodicContext = { }, permissionDetails: { periodAmount: '1', - periodType: TimePeriod.DAILY, - periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]).toString(), + periodDuration: Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]), startTime: 1729900800, }, } as const; @@ -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( @@ -297,7 +296,7 @@ describe('nativeTokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: 'invalid', + periodDuration: 'invalid' as unknown as number, }, }; @@ -315,7 +314,7 @@ describe('nativeTokenPeriodic:context', () => { ...context, permissionDetails: { ...context.permissionDetails, - periodDuration: '-1', + periodDuration: '-1' as unknown as number, }, }; 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", diff --git a/packages/gator-permissions-snap/test/utils/time.test.ts b/packages/gator-permissions-snap/test/utils/time.test.ts index 1b3e3947..8535d66b 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,89 @@ describe('Time Utility Functions', () => { expect(startOfNextDay - startOfToday).toBe(24 * 60 * 60); }); }); + + describe('getClosestTimePeriod', () => { + it('should return HOURLY for exactly 1 hour', () => { + 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 = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.DAILY]); + expect(getClosestTimePeriod(oneDay)).toBe(TimePeriod.DAILY); + }); + + it('should return WEEKLY for exactly 1 week', () => { + 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 = Number(TIME_PERIOD_TO_SECONDS[TimePeriod.BIWEEKLY]); + expect(getClosestTimePeriod(twoWeeks)).toBe(TimePeriod.BIWEEKLY); + }); + + it('should return MONTHLY for exactly 30 days', () => { + 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 = 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(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(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(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(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(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(20000000)).toBe(TimePeriod.YEARLY); // ~231 days + expect(getClosestTimePeriod(40000000)).toBe(TimePeriod.YEARLY); // ~463 days + }); + + it('should handle very small values', () => { + expect(getClosestTimePeriod(1)).toBe(TimePeriod.HOURLY); // Closest to hourly + expect(getClosestTimePeriod(100)).toBe(TimePeriod.HOURLY); + }); + + it('should handle very large values', () => { + 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 = (3600 + 86400) / 2; // 45,000 + const result = getClosestTimePeriod(halfwayHourlyDaily); + expect([TimePeriod.HOURLY, TimePeriod.DAILY]).toContain(result); + }); + + it('should accept values up to 10 years', () => { + const tenYears = 60 * 60 * 24 * 365 * 10; + expect(() => getClosestTimePeriod(tenYears)).not.toThrow(); + expect(getClosestTimePeriod(tenYears)).toBe(TimePeriod.YEARLY); + }); + }); }); 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 );