UNPKG

@anuragchvn-blip/mandatekit

Version:

Production-ready Web3 autopay SDK for crypto-based recurring payments using EIP-712 mandates

301 lines 10.1 kB
/** * 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