Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx';

import {
periodAmountRule,
periodTypeRule,
periodDurationRule,
startTimeRule,
expiryRule,
} from './rules';
Expand Down Expand Up @@ -31,7 +31,7 @@ export async function createConfirmationContent({
<Box>
<Section>
{renderRules({
rules: [startTimeRule, periodAmountRule, periodTypeRule],
rules: [startTimeRule, periodAmountRule, periodDurationRule],
context,
metadata,
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@metamask/utils';

import type { TokenMetadataService } from '../../services/tokenMetadataService';
import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time';
import { parseUnits, formatUnitsFromHex } from '../../utils/value';
import {
validateAndParseAmount,
Expand Down Expand Up @@ -177,27 +176,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 periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString();
const periodDuration = data.periodDuration.toString();

const startTime = data.startTime ?? Math.floor(Date.now() / 1000);

Expand Down Expand Up @@ -227,7 +206,6 @@ export async function buildContext({
},
permissionDetails: {
periodAmount,
periodType,
periodDuration,
startTime,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,27 +38,22 @@ export const periodAmountRule: RuleDefinition<
}),
};

export const periodTypeRule: RuleDefinition<
export const periodDurationRule: RuleDefinition<
Erc20TokenPeriodicContext,
Erc20TokenPeriodicMetadata
> = {
name: PERIOD_TYPE_ELEMENT,
label: 'Transfer Window',
label: 'Frequency',
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
Expand All @@ -84,7 +79,6 @@ export const periodTypeRule: RuleDefinition<
...context,
permissionDetails: {
...context.permissionDetails,
periodType,
periodDuration,
},
};
Expand Down Expand Up @@ -159,7 +153,7 @@ export const expiryRule: RuleDefinition<

export const allRules = [
periodAmountRule,
periodTypeRule,
periodDurationRule,
startTimeRule,
expiryRule,
];
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
DeepRequired,
TypedPermissionRequest,
BaseContext,
TimePeriod,
BaseMetadata,
} from '../../core/types';
import { zPeriodDuration } from '../../utils/time';
Expand All @@ -20,7 +19,6 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & {
validationErrors: {
periodAmountError?: string;
periodDurationError?: string;
periodTypeError?: string;
startTimeError?: string;
expiryError?: string;
};
Expand All @@ -29,7 +27,6 @@ export type Erc20TokenPeriodicMetadata = BaseMetadata & {
export type Erc20TokenPeriodicContext = BaseContext & {
permissionDetails: {
periodAmount: string;
periodType: TimePeriod;
periodDuration: string;
startTime: number;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Section } from '@metamask/snaps-sdk/jsx';

import {
periodAmountRule,
periodTypeRule,
periodDurationRule,
startTimeRule,
expiryRule,
} from './rules';
Expand Down Expand Up @@ -31,7 +31,7 @@ export async function createConfirmationContent({
<Box>
<Section>
{renderRules({
rules: [startTimeRule, periodAmountRule, periodTypeRule],
rules: [startTimeRule, periodAmountRule, periodDurationRule],
context,
metadata,
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@metamask/utils';

import type { TokenMetadataService } from '../../services/tokenMetadataService';
import { getClosestTimePeriod, TIME_PERIOD_TO_SECONDS } from '../../utils/time';
import { parseUnits, formatUnitsFromHex } from '../../utils/value';
import {
validateAndParseAmount,
Expand Down Expand Up @@ -177,27 +176,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 periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString();
const periodDuration = data.periodDuration.toString();

const startTime = data.startTime ?? Math.floor(Date.now() / 1000);

Expand Down Expand Up @@ -227,7 +206,6 @@ export async function buildContext({
},
permissionDetails: {
periodAmount,
periodType,
periodDuration,
startTime,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,27 +38,22 @@ export const periodAmountRule: RuleDefinition<
}),
};

export const periodTypeRule: RuleDefinition<
export const periodDurationRule: RuleDefinition<
NativeTokenPeriodicContext,
NativeTokenPeriodicMetadata
> = {
name: PERIOD_TYPE_ELEMENT,
label: 'Transfer Window',
label: 'Frequency',
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
Expand All @@ -84,7 +79,6 @@ export const periodTypeRule: RuleDefinition<
...context,
permissionDetails: {
...context.permissionDetails,
periodType,
periodDuration,
},
};
Expand Down Expand Up @@ -159,7 +153,7 @@ export const expiryRule: RuleDefinition<

export const allRules = [
periodAmountRule,
periodTypeRule,
periodDurationRule,
startTimeRule,
expiryRule,
];
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
DeepRequired,
TypedPermissionRequest,
BaseContext,
TimePeriod,
BaseMetadata,
} from '../../core/types';
import { zPeriodDuration } from '../../utils/time';
Expand All @@ -19,7 +18,6 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & {
validationErrors: {
periodAmountError?: string;
periodDurationError?: string;
periodTypeError?: string;
startTimeError?: string;
expiryError?: string;
};
Expand All @@ -28,7 +26,6 @@ export type NativeTokenPeriodicMetadata = BaseMetadata & {
export type NativeTokenPeriodicContext = BaseContext & {
permissionDetails: {
periodAmount: string;
periodType: TimePeriod;
periodDuration: string;
startTime: number;
};
Expand Down
50 changes: 16 additions & 34 deletions packages/gator-permissions-snap/src/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,46 +269,22 @@ export const TIME_PERIOD_TO_SECONDS: Record<TimePeriod, bigint> = {
*
* @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;
Expand All @@ -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]);
});
Loading
Loading