/** * Input validation for savings operations * * Provides validation utilities to prevent invalid inputs from reaching * cryptographic operations or blockchain transactions. */ import { Hex } from "viem"; /** * Validation utilities for savings operations */ export class SavingsValidation { /** * Validate BIP-44 account index * * @param index - Account index to validate * @throws Error if index is invalid * * @remarks * BIP-44 account indices must be: * - Integer values * - Non-negative * - Less than or equal to 2^31-1 (0x7FFFFFFF) * * @example * ```typescript * SavingsValidation.validateAccountIndex(0); // ✓ Valid * SavingsValidation.validateAccountIndex(100); // ✓ Valid * SavingsValidation.validateAccountIndex(-1); // ✗ Throws error * SavingsValidation.validateAccountIndex(2.5); // ✗ Throws error * ``` */ static validateAccountIndex(index: number): void { if (!Number.isInteger(index)) { throw new Error(`Account index must be an integer, got: ${index}`); } if (index < 0) { throw new Error(`Account index must be non-negative, got: ${index}`); } if (index > 0x7FFFFFFF) { // BIP-44 max hardened index (2^31-1) throw new Error(`Account index exceeds BIP-44 maximum (2147483647), got: ${index}`); } } /** * Validate wallet index * * @param index - Wallet index to validate * @throws Error if index is invalid */ static validateWalletIndex(index: number): void { if (!Number.isInteger(index)) { throw new Error(`Wallet index must be an integer, got: ${index}`); } if (index < 0) { throw new Error(`Wallet index must be non-negative, got: ${index}`); } if (index > 0x7FFFFFFF) { throw new Error(`Wallet index exceeds maximum (2147483647), got: ${index}`); } } /** * Validate amount (bigint) * * @param amount - Amount to validate * @param label - Label for error message (default: "Amount") * @throws Error if amount is invalid * * @example * ```typescript * SavingsValidation.validateAmount(1000n, 'Transfer amount'); // ✓ Valid * SavingsValidation.validateAmount(0n, 'Transfer amount'); // ✗ Throws error * SavingsValidation.validateAmount(-100n, 'Transfer amount'); // ✗ Throws error * ``` */ static validateAmount(amount: bigint, label: string = 'Amount'): void { if (typeof amount !== 'bigint') { throw new Error(`${label} must be a bigint, got: ${typeof amount}`); } if (amount <= 0n) { throw new Error(`${label} must be positive, got: ${amount}`); } } /** * Validate Ethereum address format * * @param address - Address to validate * @param label - Label for error message (default: "Address") * @throws Error if address format is invalid * * @example * ```typescript * SavingsValidation.validateAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); // ✓ Valid * SavingsValidation.validateAddress('0xinvalid'); // ✗ Throws error * SavingsValidation.validateAddress('742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); // ✗ Throws error (no 0x) * ``` */ static validateAddress(address: string, label: string = 'Address'): void { if (typeof address !== 'string') { throw new Error(`${label} must be a string, got: ${typeof address}`); } if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { throw new Error(`${label} has invalid format. Expected 0x followed by 40 hex characters, got: ${address}`); } } /** * Validate mnemonic phrase * * @param mnemonic - Mnemonic to validate * @throws Error if mnemonic is invalid * * @remarks * Performs basic validation: * - Must be a non-empty string * - Must have 12, 15, 18, 21, or 24 words (BIP-39 standard) * - Does NOT validate checksum (implementer should use bip39 library for that) */ static validateMnemonic(mnemonic: string): void { if (typeof mnemonic !== 'string') { throw new Error(`Mnemonic must be a string, got: ${typeof mnemonic}`); } const trimmed = mnemonic.trim(); if (trimmed.length === 0) { throw new Error('Mnemonic cannot be empty'); } const words = trimmed.split(/\s+/); const validWordCounts = [12, 15, 18, 21, 24]; if (!validWordCounts.includes(words.length)) { throw new Error( `Mnemonic must have 12, 15, 18, 21, or 24 words (BIP-39 standard), got: ${words.length} words` ); } } /** * Validate chain ID * * @param chainId - Chain ID to validate * @throws Error if chain ID is invalid */ static validateChainId(chainId: number): void { if (!Number.isInteger(chainId)) { throw new Error(`Chain ID must be an integer, got: ${chainId}`); } if (chainId <= 0) { throw new Error(`Chain ID must be positive, got: ${chainId}`); } } /** * Validate string amount (for parsing) * * @param amount - String amount to validate * @param label - Label for error message * @throws Error if amount string is invalid */ static validateAmountString(amount: string, label: string = 'Amount'): void { if (typeof amount !== 'string') { throw new Error(`${label} must be a string, got: ${typeof amount}`); } const trimmed = amount.trim(); if (trimmed.length === 0) { throw new Error(`${label} cannot be empty`); } // Check if it's a valid number string if (!/^-?\d+(\.\d+)?$/.test(trimmed)) { throw new Error(`${label} has invalid format, got: ${amount}`); } const parsed = parseFloat(trimmed); if (isNaN(parsed)) { throw new Error(`${label} cannot be parsed as number, got: ${amount}`); } if (parsed <= 0) { throw new Error(`${label} must be positive, got: ${amount}`); } } }