@anuragchvn-blip/mandatekit
Version:
Production-ready Web3 autopay SDK for crypto-based recurring payments using EIP-712 mandates
257 lines • 9.08 kB
JavaScript
/**
* Validation utilities for MandateKit SDK
* Provides comprehensive validation for mandates and related data
* @module utils/validation
*/
import { ValidationError, ExpiredMandateError, MandateNotYetValidError, MaxPaymentsReachedError, } from '../errors/index.js';
import { validateCadence, isPaymentDue } from './cadence.js';
import { isValidAddress, isValidSignature } from './crypto.js';
/**
* Validates a mandate structure for completeness and correctness
*
* @param mandate - Mandate to validate
* @throws {ValidationError} If any field is invalid
*
* @example
* ```typescript
* try {
* validateMandate(mandate);
* console.log('Mandate is valid');
* } catch (error) {
* console.error('Validation failed:', error.message);
* }
* ```
*/
export function validateMandate(mandate) {
// Validate subscriber address
if (!mandate.subscriber || !isValidAddress(mandate.subscriber)) {
throw new ValidationError('Invalid subscriber address', 'subscriber');
}
// Validate token address
if (!mandate.token || !isValidAddress(mandate.token)) {
throw new ValidationError('Invalid token address', 'token');
}
// Validate recipient address
if (!mandate.recipient || !isValidAddress(mandate.recipient)) {
throw new ValidationError('Invalid recipient address', 'recipient');
}
// Validate amount
const amount = BigInt(mandate.amount);
if (amount <= 0n) {
throw new ValidationError('Amount must be greater than zero', 'amount');
}
// Validate cadence
validateCadence(mandate.cadence);
// Validate timestamps
if (!Number.isInteger(mandate.validAfter) || mandate.validAfter < 0) {
throw new ValidationError('validAfter must be a valid unix timestamp', 'validAfter');
}
if (!Number.isInteger(mandate.validBefore) || mandate.validBefore < 0) {
throw new ValidationError('validBefore must be a valid unix timestamp', 'validBefore');
}
if (mandate.validBefore <= mandate.validAfter) {
throw new ValidationError('validBefore must be after validAfter', 'validBefore');
}
// Validate nonce
const nonce = BigInt(mandate.nonce);
if (nonce < 0n) {
throw new ValidationError('Nonce must be non-negative', 'nonce');
}
// Validate maxPayments if provided
if (mandate.maxPayments !== undefined) {
if (!Number.isInteger(mandate.maxPayments) || mandate.maxPayments < 1) {
throw new ValidationError('maxPayments must be a positive integer', 'maxPayments');
}
}
// Validate metadata if provided
if (mandate.metadata !== undefined) {
if (typeof mandate.metadata !== 'string') {
throw new ValidationError('metadata must be a string', 'metadata');
}
if (mandate.metadata.length > 1000) {
throw new ValidationError('metadata exceeds maximum length of 1000 characters', 'metadata');
}
}
}
/**
* Validates a signed mandate including signature format
*
* @param signedMandate - Signed mandate to validate
* @throws {ValidationError} If mandate or signature is invalid
*/
export function validateSignedMandate(signedMandate) {
// First validate the base mandate
validateMandate(signedMandate);
// Validate signature format
if (!signedMandate.signature || !isValidSignature(signedMandate.signature)) {
throw new ValidationError('Invalid signature format', 'signature');
}
// Validate signedAt timestamp
if (!Number.isInteger(signedMandate.signedAt) || signedMandate.signedAt < 0) {
throw new ValidationError('signedAt must be a valid unix timestamp', 'signedAt');
}
// Validate chainId
if (!Number.isInteger(signedMandate.chainId) || signedMandate.chainId < 1) {
throw new ValidationError('chainId must be a positive integer', 'chainId');
}
}
/**
* Checks if a mandate is currently active based on timestamps
*
* @param mandate - Mandate to check
* @param currentTime - Current unix timestamp (defaults to now)
* @returns True if mandate is within valid time window
*/
export function isMandateActive(mandate, currentTime = Math.floor(Date.now() / 1000)) {
return currentTime >= mandate.validAfter && currentTime < mandate.validBefore;
}
/**
* Validates that a mandate is active, throwing descriptive errors if not
*
* @param mandate - Mandate to validate
* @param currentTime - Current unix timestamp (defaults to now)
* @throws {MandateNotYetValidError} If before validAfter
* @throws {ExpiredMandateError} If after validBefore
*/
export function validateMandateActive(mandate, currentTime = Math.floor(Date.now() / 1000)) {
if (currentTime < mandate.validAfter) {
throw new MandateNotYetValidError(mandate.validAfter, currentTime);
}
if (currentTime >= mandate.validBefore) {
throw new ExpiredMandateError(mandate.validBefore, currentTime);
}
}
/**
* Validates execution count against maxPayments limit
*
* @param mandate - Mandate with potential maxPayments limit
* @param executionCount - Current number of executed payments
* @throws {MaxPaymentsReachedError} If limit exceeded
*/
export function validateExecutionCount(mandate, executionCount) {
if (mandate.maxPayments && executionCount >= mandate.maxPayments) {
throw new MaxPaymentsReachedError(mandate.maxPayments, executionCount);
}
}
/**
* Performs comprehensive validation of a mandate for execution
* Combines multiple validation checks
*
* @param mandate - Mandate to validate
* @param executionCount - Current number of executed payments
* @param lastExecutionTime - Timestamp of last execution (if any)
* @param currentTime - Current unix timestamp (defaults to now)
* @returns Verification result with detailed status
*
* @example
* ```typescript
* const result = verifyMandateForExecution(
* mandate,
* executionCount,
* lastExecutionTime
* );
* if (result.isValid && result.isExecutionDue) {
* // Safe to execute payment
* }
* ```
*/
export function verifyMandateForExecution(mandate, executionCount, lastExecutionTime, currentTime = Math.floor(Date.now() / 1000)) {
const errors = [];
let isValid = true;
let isActive = true;
let isWithinTimeRange = true;
let isExecutionDue = true;
try {
validateMandate(mandate);
}
catch (error) {
isValid = false;
errors.push(`Validation failed: ${error.message}`);
}
try {
validateMandateActive(mandate, currentTime);
}
catch (error) {
isActive = false;
isWithinTimeRange = false;
errors.push(`Mandate inactive: ${error.message}`);
}
try {
validateExecutionCount(mandate, executionCount);
}
catch (error) {
isActive = false;
errors.push(`Max payments reached: ${error.message}`);
}
// Check if payment is due (only if there was a previous execution)
if (lastExecutionTime) {
const due = isPaymentDue(lastExecutionTime, mandate.cadence, currentTime);
if (!due) {
isExecutionDue = false;
errors.push('Payment is not yet due according to cadence');
}
}
return {
isValid: isValid && errors.length === 0,
isActive,
isWithinTimeRange,
isExecutionDue,
errors,
};
}
/**
* Checks if two addresses are equal (case-insensitive)
*
* @param address1 - First address
* @param address2 - Second address
* @returns True if addresses are equal
*/
export function addressesEqual(address1, address2) {
return address1.toLowerCase() === address2.toLowerCase();
}
/**
* Validates that an amount is sufficient for a payment
*
* @param available - Available balance
* @param required - Required amount
* @param tokenSymbol - Token symbol for error message
* @throws {ValidationError} If balance is insufficient
*/
export function validateSufficientBalance(available, required, tokenSymbol = 'tokens') {
if (available < required) {
throw new ValidationError(`Insufficient balance: have ${available.toString()} ${tokenSymbol}, need ${required.toString()} ${tokenSymbol}`);
}
}
/**
* Sanitizes metadata string to prevent injection attacks
*
* @param metadata - Metadata string to sanitize
* @returns Sanitized metadata
*/
export function sanitizeMetadata(metadata) {
// Remove control characters and limit length
return metadata
.split('')
.filter(char => {
const code = char.charCodeAt(0);
return code > 0x1F && code !== 0x7F;
})
.join('')
.slice(0, 1000); // Limit length
}
/**
* Parses amount string to bigint, handling various formats
*
* @param amount - Amount as string or bigint
* @returns Parsed bigint amount
* @throws {ValidationError} If amount cannot be parsed
*/
export function parseAmount(amount) {
try {
return BigInt(amount);
}
catch (error) {
throw new ValidationError(`Invalid amount format: ${amount}`, 'amount');
}
}
//# sourceMappingURL=validation.js.map