Skip to content

Commit 15f94a3

Browse files
committed
Enhance time period validation and error handling in ERC20 and native token contexts; add tests for edge cases in getClosestTimePeriod function
1 parent 8673a95 commit 15f94a3

File tree

8 files changed

+134
-1082
lines changed

8 files changed

+134
-1082
lines changed

packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/context.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,26 @@ export async function buildContext({
177177
decimals,
178178
});
179179

180-
const periodType = getClosestTimePeriod(BigInt(data.periodDuration));
180+
// Safely convert period duration to BigInt with error handling
181+
let periodDurationBigInt: bigint;
182+
try {
183+
periodDurationBigInt = BigInt(data.periodDuration);
184+
} catch (error) {
185+
throw new InvalidInputError(
186+
`Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`,
187+
);
188+
}
189+
190+
// Validate that the duration is positive
191+
if (periodDurationBigInt <= 0n) {
192+
throw new InvalidInputError(
193+
`Period duration must be positive. Received: ${periodDurationBigInt} seconds.`,
194+
);
195+
}
196+
197+
// Map the requested duration to the closest standard time period.
198+
// This normalizes non-standard durations to predefined periods.
199+
const periodType = getClosestTimePeriod(periodDurationBigInt);
181200
const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString();
182201

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

packages/gator-permissions-snap/src/permissions/erc20TokenPeriodic/rules.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { InvalidInputError } from '@metamask/snaps-sdk';
2+
13
import { TimePeriod } from '../../core/types';
24
import type { RuleDefinition } from '../../core/types';
35
import { TIME_PERIOD_TO_SECONDS } from '../../utils/time';
@@ -9,7 +11,6 @@ import type {
911

1012
export const PERIOD_AMOUNT_ELEMENT = 'erc20-token-periodic-period-amount';
1113
export const PERIOD_TYPE_ELEMENT = 'erc20-token-periodic-period-type';
12-
export const PERIOD_DURATION_ELEMENT = 'erc20-token-periodic-period-duration';
1314
export const START_TIME_ELEMENT = 'erc20-token-periodic-start-date';
1415
export const EXPIRY_ELEMENT = 'erc20-token-periodic-expiry';
1516

@@ -60,10 +61,24 @@ export const periodTypeRule: RuleDefinition<
6061
error: metadata.validationErrors.periodTypeError,
6162
}),
6263
updateContext: (context: Erc20TokenPeriodicContext, value: string) => {
64+
// Validate that value is a valid TimePeriod
65+
if (!Object.values(TimePeriod).includes(value as TimePeriod)) {
66+
throw new InvalidInputError(
67+
`Invalid period type: "${value}". Valid options are: ${Object.values(TimePeriod).join(', ')}`,
68+
);
69+
}
70+
6371
const periodType = value as TimePeriod;
64-
const periodDuration = Number(
65-
TIME_PERIOD_TO_SECONDS[periodType],
66-
).toString();
72+
const periodSeconds = TIME_PERIOD_TO_SECONDS[periodType];
73+
74+
// This should never happen if the above check passed, but be defensive
75+
if (periodSeconds === undefined) {
76+
throw new InvalidInputError(
77+
`Period type "${periodType}" is not mapped to a duration. This indicates a system error.`,
78+
);
79+
}
80+
81+
const periodDuration = Number(periodSeconds).toString();
6782

6883
return {
6984
...context,

packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/context.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,26 @@ export async function buildContext({
177177
decimals,
178178
});
179179

180-
const periodType = getClosestTimePeriod(BigInt(data.periodDuration));
180+
// Safely convert period duration to BigInt with error handling
181+
let periodDurationBigInt: bigint;
182+
try {
183+
periodDurationBigInt = BigInt(data.periodDuration);
184+
} catch (error) {
185+
throw new InvalidInputError(
186+
`Invalid period duration: "${data.periodDuration}". Period duration must be a valid integer representing seconds.`,
187+
);
188+
}
189+
190+
// Validate that the duration is positive
191+
if (periodDurationBigInt <= 0n) {
192+
throw new InvalidInputError(
193+
`Period duration must be positive. Received: ${periodDurationBigInt} seconds.`,
194+
);
195+
}
196+
197+
// Map the requested duration to the closest standard time period.
198+
// This normalizes non-standard durations to predefined periods.
199+
const periodType = getClosestTimePeriod(periodDurationBigInt);
181200
const periodDuration = TIME_PERIOD_TO_SECONDS[periodType].toString();
182201

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

packages/gator-permissions-snap/src/permissions/nativeTokenPeriodic/rules.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { InvalidInputError } from '@metamask/snaps-sdk';
2+
13
import { TimePeriod } from '../../core/types';
24
import type { RuleDefinition } from '../../core/types';
35
import { TIME_PERIOD_TO_SECONDS } from '../../utils/time';
@@ -9,7 +11,6 @@ import type {
911

1012
export const PERIOD_AMOUNT_ELEMENT = 'native-token-periodic-period-amount';
1113
export const PERIOD_TYPE_ELEMENT = 'native-token-periodic-period-type';
12-
export const PERIOD_DURATION_ELEMENT = 'native-token-periodic-period-duration';
1314
export const START_TIME_ELEMENT = 'native-token-periodic-start-date';
1415
export const EXPIRY_ELEMENT = 'native-token-periodic-expiry';
1516

@@ -60,10 +61,24 @@ export const periodTypeRule: RuleDefinition<
6061
error: metadata.validationErrors.periodTypeError,
6162
}),
6263
updateContext: (context: NativeTokenPeriodicContext, value: string) => {
64+
// Validate that value is a valid TimePeriod
65+
if (!Object.values(TimePeriod).includes(value as TimePeriod)) {
66+
throw new InvalidInputError(
67+
`Invalid period type: "${value}". Valid options are: ${Object.values(TimePeriod).join(', ')}`,
68+
);
69+
}
70+
6371
const periodType = value as TimePeriod;
64-
const periodDuration = Number(
65-
TIME_PERIOD_TO_SECONDS[periodType],
66-
).toString();
72+
const periodSeconds = TIME_PERIOD_TO_SECONDS[periodType];
73+
74+
// This should never happen if the above check passed, but be defensive
75+
if (periodSeconds === undefined) {
76+
throw new InvalidInputError(
77+
`Period type "${periodType}" is not mapped to a duration. This indicates a system error.`,
78+
);
79+
}
80+
81+
const periodDuration = Number(periodSeconds).toString();
6782

6883
return {
6984
...context,

packages/gator-permissions-snap/src/utils/time.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,30 +253,52 @@ export const getStartOfNextDayUTC = (): number => {
253253
* A mapping of time periods to their equivalent seconds.
254254
*/
255255
export const TIME_PERIOD_TO_SECONDS: Record<TimePeriod, bigint> = {
256-
[TimePeriod.HOURLY]: 60n * 60n, // 3,600(seconds)
257-
[TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400(seconds)
258-
[TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800(seconds), 7 days
259-
[TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600(seconds), 14 days
260-
[TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000(seconds), 30 days
261-
[TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000(seconds), 365 days
256+
[TimePeriod.HOURLY]: 60n * 60n, // 3,600 seconds (1 hour)
257+
[TimePeriod.DAILY]: 60n * 60n * 24n, // 86,400 seconds (1 day)
258+
[TimePeriod.WEEKLY]: 60n * 60n * 24n * 7n, // 604,800 seconds (7 days)
259+
[TimePeriod.BIWEEKLY]: 60n * 60n * 24n * 14n, // 1,209,600 seconds (14 days)
260+
[TimePeriod.MONTHLY]: 60n * 60n * 24n * 30n, // 2,592,000 seconds (approximated as 30 days, real months vary 28-31 days)
261+
[TimePeriod.YEARLY]: 60n * 60n * 24n * 365n, // 31,536,000 seconds (365 days, does not account for leap years)
262262
};
263263

264264
/**
265265
* Finds the closest TimePeriod enum value for a given duration in seconds.
266+
* Uses absolute difference to find the nearest match by comparing against all
267+
* predefined time periods (HOURLY, DAILY, WEEKLY, BIWEEKLY, MONTHLY, YEARLY).
266268
*
267-
* @param seconds - The duration in seconds to match.
269+
* @param seconds - The duration in seconds to match. Must be positive and reasonable.
268270
* @returns The TimePeriod that most closely matches the given duration.
269-
* @throws InvalidInputError if no time periods are available.
271+
* @throws InvalidInputError if seconds is invalid or no time periods are available.
272+
* @example
273+
* getClosestTimePeriod(80000n) // Returns TimePeriod.DAILY (~22 hours)
274+
* getClosestTimePeriod(1300000n) // Returns TimePeriod.BIWEEKLY (~15 days)
270275
*/
271276
export const getClosestTimePeriod = (seconds: bigint): TimePeriod => {
277+
// Validate input range
278+
if (seconds <= 0n) {
279+
throw new InvalidInputError(
280+
`Period duration must be positive. Received: ${seconds} seconds.`,
281+
);
282+
}
283+
284+
// Warn about absurdly large values (more than 10 years)
285+
const TEN_YEARS = 60n * 60n * 24n * 365n * 10n;
286+
if (seconds > TEN_YEARS) {
287+
throw new InvalidInputError(
288+
`Period duration ${seconds} seconds (${seconds / (60n * 60n * 24n)} days) is too large. Maximum supported period is 10 years.`,
289+
);
290+
}
291+
272292
const timePeriodEntries = Object.entries(TIME_PERIOD_TO_SECONDS) as [
273293
TimePeriod,
274294
bigint,
275295
][];
276296

277297
const firstEntry = timePeriodEntries[0];
278298
if (!firstEntry) {
279-
throw new InvalidInputError('No time periods available');
299+
throw new InvalidInputError(
300+
`No time periods available. This indicates a system error. Input: ${seconds} seconds, Available periods: ${Object.keys(TIME_PERIOD_TO_SECONDS).length}`,
301+
);
280302
}
281303

282304
let closestPeriod = firstEntry[0];

0 commit comments

Comments
 (0)