@anuragchvn-blip/mandatekit
Version:
Production-ready Web3 autopay SDK for crypto-based recurring payments using EIP-712 mandates
301 lines • 10.1 kB
JavaScript
/**
* Cadence enforcement utilities for MandateKit SDK
* Handles payment scheduling, interval calculation, and execution validation
* @module utils/cadence
*/
import { ValidationError, ExecutionTooEarlyError } from '../errors/index.js';
/**
* Seconds in common time intervals (for calculations)
*/
export const TIME_CONSTANTS = {
SECOND: 1,
MINUTE: 60,
HOUR: 3600,
DAY: 86400,
WEEK: 604800,
MONTH: 2592000, // 30 days average
YEAR: 31536000, // 365 days
};
/**
* Converts a cadence configuration to seconds
*
* @param cadence - Cadence configuration
* @returns Number of seconds between payments
*
* @example
* ```typescript
* const seconds = cadenceToSeconds({ interval: 'weekly', count: 2 });
* // Returns: 1209600 (2 weeks in seconds)
* ```
*/
export function cadenceToSeconds(cadence) {
if (cadence.interval === 'custom') {
if (!cadence.customSeconds || cadence.customSeconds <= 0) {
throw new ValidationError('Custom cadence requires a positive customSeconds value', 'cadence.customSeconds');
}
return cadence.customSeconds * cadence.count;
}
const baseSeconds = {
daily: TIME_CONSTANTS.DAY,
weekly: TIME_CONSTANTS.WEEK,
monthly: TIME_CONSTANTS.MONTH,
yearly: TIME_CONSTANTS.YEAR,
};
const base = baseSeconds[cadence.interval];
return base * cadence.count;
}
/**
* Calculates the next execution timestamp based on last execution and cadence
*
* @param lastExecutionTime - Unix timestamp of last payment
* @param cadence - Cadence configuration
* @returns Unix timestamp of next allowed execution
*
* @example
* ```typescript
* const lastExecution = 1704067200; // Jan 1, 2024
* const next = calculateNextExecution(lastExecution, {
* interval: 'monthly',
* count: 1
* });
* // Returns: ~1706745600 (Feb 1, 2024)
* ```
*/
export function calculateNextExecution(lastExecutionTime, cadence) {
const intervalSeconds = cadenceToSeconds(cadence);
return lastExecutionTime + intervalSeconds;
}
/**
* Checks if a payment is due based on last execution and cadence
*
* @param lastExecutionTime - Unix timestamp of last payment
* @param cadence - Cadence configuration
* @param currentTime - Current unix timestamp (defaults to now)
* @returns True if payment is due
*
* @example
* ```typescript
* const isDue = isPaymentDue(
* lastExecutionTimestamp,
* { interval: 'daily', count: 1 },
* Date.now() / 1000
* );
* ```
*/
export function isPaymentDue(lastExecutionTime, cadence, currentTime = Math.floor(Date.now() / 1000)) {
const nextExecution = calculateNextExecution(lastExecutionTime, cadence);
return currentTime >= nextExecution;
}
/**
* Validates that a payment can be executed now
* Throws descriptive errors if execution is not allowed
*
* @param lastExecutionTime - Unix timestamp of last payment
* @param cadence - Cadence configuration
* @param currentTime - Current unix timestamp (defaults to now)
* @throws {ExecutionTooEarlyError} If trying to execute before next scheduled time
*
* @example
* ```typescript
* try {
* validateExecutionTiming(lastExecution, cadence);
* // Safe to execute
* } catch (error) {
* if (error instanceof ExecutionTooEarlyError) {
* console.log('Next execution at:', error.details.nextExecutionTime);
* }
* }
* ```
*/
export function validateExecutionTiming(lastExecutionTime, cadence, currentTime = Math.floor(Date.now() / 1000)) {
const nextExecution = calculateNextExecution(lastExecutionTime, cadence);
if (currentTime < nextExecution) {
throw new ExecutionTooEarlyError(nextExecution, currentTime);
}
}
/**
* Calculates time remaining until next execution
*
* @param lastExecutionTime - Unix timestamp of last payment
* @param cadence - Cadence configuration
* @param currentTime - Current unix timestamp (defaults to now)
* @returns Seconds until next execution (0 if already due)
*
* @example
* ```typescript
* const secondsRemaining = getTimeUntilNextExecution(
* lastExecution,
* { interval: 'weekly', count: 1 }
* );
* console.log(`Next payment in ${secondsRemaining} seconds`);
* ```
*/
export function getTimeUntilNextExecution(lastExecutionTime, cadence, currentTime = Math.floor(Date.now() / 1000)) {
const nextExecution = calculateNextExecution(lastExecutionTime, cadence);
const remaining = nextExecution - currentTime;
return Math.max(0, remaining);
}
/**
* Validates cadence configuration
*
* @param cadence - Cadence to validate
* @throws {ValidationError} If cadence is invalid
*
* @example
* ```typescript
* validateCadence({ interval: 'monthly', count: 1 }); // OK
* validateCadence({ interval: 'daily', count: 0 }); // Throws ValidationError
* ```
*/
export function validateCadence(cadence) {
if (!cadence.interval) {
throw new ValidationError('Cadence interval is required', 'cadence.interval');
}
const validIntervals = ['daily', 'weekly', 'monthly', 'yearly', 'custom'];
if (!validIntervals.includes(cadence.interval)) {
throw new ValidationError(`Invalid cadence interval: ${cadence.interval}. Must be one of: ${validIntervals.join(', ')}`, 'cadence.interval');
}
if (!Number.isInteger(cadence.count) || cadence.count < 1) {
throw new ValidationError('Cadence count must be a positive integer', 'cadence.count');
}
if (cadence.interval === 'custom') {
if (!cadence.customSeconds) {
throw new ValidationError('Custom cadence requires customSeconds to be specified', 'cadence.customSeconds');
}
if (!Number.isInteger(cadence.customSeconds) || cadence.customSeconds < 1) {
throw new ValidationError('Custom seconds must be a positive integer', 'cadence.customSeconds');
}
}
}
/**
* Generates a human-readable description of a cadence
*
* @param cadence - Cadence configuration
* @returns Human-friendly description string
*
* @example
* ```typescript
* describeCadence({ interval: 'monthly', count: 1 });
* // Returns: "Every month"
*
* describeCadence({ interval: 'weekly', count: 2 });
* // Returns: "Every 2 weeks"
* ```
*/
export function describeCadence(cadence) {
if (cadence.interval === 'custom') {
const seconds = cadence.customSeconds ?? 0;
return `Every ${seconds} seconds`;
}
const intervalName = cadence.interval.replace('ly', '');
if (cadence.count === 1) {
return `Every ${intervalName}`;
}
const pluralMap = {
dai: 'days',
week: 'weeks',
month: 'months',
year: 'years',
};
const plural = pluralMap[intervalName] ?? `${intervalName}s`;
return `Every ${cadence.count} ${plural}`;
}
/**
* Calculates all scheduled execution times between two dates
* Useful for generating payment calendars
*
* @param startTime - First execution timestamp
* @param endTime - End timestamp
* @param cadence - Cadence configuration
* @returns Array of scheduled execution timestamps
*
* @example
* ```typescript
* const schedule = generateExecutionSchedule(
* startTime,
* endTime,
* { interval: 'weekly', count: 1 }
* );
* // Returns: [timestamp1, timestamp2, timestamp3, ...]
* ```
*/
export function generateExecutionSchedule(startTime, endTime, cadence) {
validateCadence(cadence);
if (startTime >= endTime) {
throw new ValidationError('Start time must be before end time');
}
const schedule = [];
let currentExecution = startTime;
const intervalSeconds = cadenceToSeconds(cadence);
while (currentExecution <= endTime) {
schedule.push(currentExecution);
currentExecution += intervalSeconds;
}
return schedule;
}
/**
* Calculates execution count based on elapsed time
* Useful for estimating how many payments should have occurred
*
* @param firstExecutionTime - Timestamp of first payment
* @param currentTime - Current timestamp
* @param cadence - Cadence configuration
* @returns Number of payments that should have been executed
*
* @example
* ```typescript
* const expectedCount = calculateExpectedExecutionCount(
* startTime,
* Date.now() / 1000,
* { interval: 'monthly', count: 1 }
* );
* ```
*/
export function calculateExpectedExecutionCount(firstExecutionTime, currentTime, cadence) {
if (currentTime < firstExecutionTime) {
return 0;
}
const elapsed = currentTime - firstExecutionTime;
const intervalSeconds = cadenceToSeconds(cadence);
return Math.floor(elapsed / intervalSeconds) + 1; // +1 for the first execution
}
/**
* Checks if a cadence represents a reasonable payment frequency
* Returns warnings for unusual configurations
*
* @param cadence - Cadence to check
* @returns Validation result with warnings
*
* @example
* ```typescript
* const result = checkCadenceReasonableness({ interval: 'daily', count: 365 });
* // result.warnings: ["Cadence exceeds 1 year, consider using yearly interval"]
* ```
*/
export function checkCadenceReasonableness(cadence) {
const warnings = [];
const seconds = cadenceToSeconds(cadence);
// Too frequent (less than 1 hour)
if (seconds < TIME_CONSTANTS.HOUR) {
warnings.push('Cadence is less than 1 hour. Very frequent payments may incur high gas costs.');
}
// Too infrequent (more than 5 years)
if (seconds > TIME_CONSTANTS.YEAR * 5) {
warnings.push('Cadence exceeds 5 years. Consider if this is appropriate for your use case.');
}
// Could use simpler interval
if (cadence.interval === 'daily' && cadence.count === 7) {
warnings.push('Consider using weekly interval instead of 7 days');
}
if (cadence.interval === 'daily' && cadence.count === 30) {
warnings.push('Consider using monthly interval instead of 30 days');
}
if (cadence.interval === 'monthly' && cadence.count === 12) {
warnings.push('Consider using yearly interval instead of 12 months');
}
return {
isReasonable: warnings.length === 0,
warnings,
};
}
//# sourceMappingURL=cadence.js.map