UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

322 lines (276 loc) 9.53 kB
/** * @fileoverview Comprehensive validation utilities for Bitcoin operations * @version 1.0.0 * @author yfbsei * @license ISC */ import { CRYPTO_CONSTANTS } from '../core/constants.js'; /** * Custom error class for validation failures * @class ValidationError * @extends Error */ class ValidationError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'ValidationError'; this.code = code; this.details = details; } } /** * Validate a value is a Buffer with optional length check * @param {*} value - Value to validate * @param {number|null} [expectedLength=null] - Expected byte length * @param {string} [fieldName='value'] - Field name for error messages * @returns {boolean} True if valid * @throws {ValidationError} If validation fails */ function validateBuffer(value, expectedLength = null, fieldName = 'value') { if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) { throw new ValidationError( `${fieldName} must be a Buffer or Uint8Array`, 'INVALID_TYPE', { fieldName, actualType: typeof value } ); } if (expectedLength !== null && value.length !== expectedLength) { throw new ValidationError( `${fieldName} must be ${expectedLength} bytes, got ${value.length}`, 'INVALID_LENGTH', { fieldName, expected: expectedLength, actual: value.length } ); } return true; } /** * Validate buffer length is within range * @param {Buffer} value - Buffer to validate * @param {number} minLength - Minimum length * @param {number} maxLength - Maximum length * @param {string} [fieldName='value'] - Field name * @returns {boolean} True if valid */ function validateBufferLength(value, minLength, maxLength, fieldName = 'value') { validateBuffer(value, null, fieldName); if (value.length < minLength || value.length > maxLength) { throw new ValidationError( `${fieldName} length must be between ${minLength} and ${maxLength} bytes`, 'INVALID_LENGTH', { fieldName, min: minLength, max: maxLength, actual: value.length } ); } return true; } /** * Validate number is within range * @param {number} value - Number to validate * @param {number} min - Minimum value * @param {number} max - Maximum value * @param {string} [fieldName='value'] - Field name * @returns {boolean} True if valid */ function validateNumberRange(value, min, max, fieldName = 'value') { if (typeof value !== 'number' || isNaN(value)) { throw new ValidationError( `${fieldName} must be a valid number`, 'INVALID_TYPE', { fieldName, actualType: typeof value } ); } if (value < min || value > max) { throw new ValidationError( `${fieldName} must be between ${min} and ${max}`, 'OUT_OF_RANGE', { fieldName, min, max, actual: value } ); } return true; } /** * Validate a hex string * @param {string} value - String to validate * @param {number|null} [expectedLength=null] - Expected length * @param {string} [fieldName='value'] - Field name * @returns {boolean} True if valid */ function validateHexString(value, expectedLength = null, fieldName = 'value') { if (typeof value !== 'string') { throw new ValidationError( `${fieldName} must be a string`, 'INVALID_TYPE', { fieldName, actualType: typeof value } ); } const hexRegex = /^[0-9a-fA-F]*$/; if (!hexRegex.test(value)) { throw new ValidationError( `${fieldName} must be a valid hex string`, 'INVALID_HEX', { fieldName } ); } if (expectedLength !== null && value.length !== expectedLength) { throw new ValidationError( `${fieldName} must be ${expectedLength} characters`, 'INVALID_LENGTH', { fieldName, expected: expectedLength, actual: value.length } ); } return true; } /** * Validate any Bitcoin address (auto-detects type) * @param {string} address - Address to validate * @param {string} [network='main'] - Network type * @returns {Object} Validation result */ function validateAddress(address, network = 'main') { if (typeof address !== 'string' || address.length === 0) { throw new ValidationError('Address must be a non-empty string', 'INVALID_ADDRESS'); } const lowerAddr = address.toLowerCase(); const upperAddr = address.toUpperCase(); if (lowerAddr.startsWith('bc1') || lowerAddr.startsWith('tb1')) { return validateBech32Address(address, network); } if (address.startsWith('1') || address.startsWith('3') || address.startsWith('m') || address.startsWith('n') || address.startsWith('2')) { return validateBase58Address(address, network); } throw new ValidationError('Unknown address format', 'UNKNOWN_FORMAT', { address }); } function validateBech32Address(address, network = 'main') { const lowerAddr = address.toLowerCase(); const expectedPrefix = network === 'main' ? 'bc1' : 'tb1'; if (!lowerAddr.startsWith(expectedPrefix)) { throw new ValidationError( `Invalid prefix for ${network} network`, 'INVALID_PREFIX', { expected: expectedPrefix, actual: lowerAddr.slice(0, 3) } ); } if (address.length < 14 || address.length > 74) { throw new ValidationError( 'Invalid bech32 address length', 'INVALID_LENGTH', { length: address.length } ); } return { valid: true, type: 'bech32', network }; } function validateBase58Address(address, network = 'main') { const base58Regex = /^[1-9A-HJ-NP-Za-km-z]+$/; if (!base58Regex.test(address)) { throw new ValidationError('Invalid Base58 characters', 'INVALID_CHARACTERS'); } if (address.length < 25 || address.length > 35) { throw new ValidationError( 'Invalid Base58 address length', 'INVALID_LENGTH', { length: address.length } ); } return { valid: true, type: 'base58', network }; } /** * Validate a private key * @param {string|Buffer} key - Private key * @param {string} [fieldName='private key'] - Field name * @returns {Object} Validation result with format */ function validatePrivateKey(key, fieldName = 'private key') { if (typeof key === 'string') { if (/^[0-9a-fA-F]{64}$/.test(key)) { return { valid: true, format: 'hex' }; } if (key.startsWith('5') || key.startsWith('K') || key.startsWith('L') || key.startsWith('9') || key.startsWith('c')) { return { valid: true, format: 'wif' }; } throw new ValidationError(`Invalid ${fieldName} format`, 'INVALID_FORMAT'); } if (Buffer.isBuffer(key) || key instanceof Uint8Array) { if (key.length !== CRYPTO_CONSTANTS.PRIVATE_KEY_LENGTH) { throw new ValidationError( `${fieldName} must be ${CRYPTO_CONSTANTS.PRIVATE_KEY_LENGTH} bytes`, 'INVALID_LENGTH' ); } return { valid: true, format: 'buffer' }; } throw new ValidationError(`${fieldName} must be string or Buffer`, 'INVALID_TYPE'); } /** * Validate a public key * @param {string|Buffer} key - Public key * @param {string} [fieldName='public key'] - Field name * @returns {Object} Validation result with compressed flag */ function validatePublicKey(key, fieldName = 'public key') { let keyBuffer; if (typeof key === 'string') { keyBuffer = Buffer.from(key, 'hex'); } else if (Buffer.isBuffer(key) || key instanceof Uint8Array) { keyBuffer = Buffer.from(key); } else { throw new ValidationError(`${fieldName} must be string or Buffer`, 'INVALID_TYPE'); } if (keyBuffer.length === 33) { if (keyBuffer[0] !== 0x02 && keyBuffer[0] !== 0x03) { throw new ValidationError('Invalid compressed public key prefix', 'INVALID_PREFIX'); } return { valid: true, compressed: true }; } if (keyBuffer.length === 65) { if (keyBuffer[0] !== 0x04) { throw new ValidationError('Invalid uncompressed public key prefix', 'INVALID_PREFIX'); } return { valid: true, compressed: false }; } throw new ValidationError( `${fieldName} must be 33 (compressed) or 65 (uncompressed) bytes`, 'INVALID_LENGTH' ); } function validateDerivationPath(path) { if (typeof path !== 'string') { throw new ValidationError('Derivation path must be a string', 'INVALID_TYPE'); } const pathRegex = /^m(\/\d+'?)*$/; if (!pathRegex.test(path)) { throw new ValidationError('Invalid derivation path format', 'INVALID_FORMAT', { path }); } return { valid: true }; } function validateNetwork(network) { const validNetworks = ['main', 'test', 'mainnet', 'testnet']; if (!validNetworks.includes(network)) { throw new ValidationError( `Invalid network: ${network}. Use 'main' or 'test'`, 'INVALID_NETWORK' ); } return network === 'main' || network === 'mainnet' ? 'main' : 'test'; } function assertValid(condition, message, code = 'ASSERTION_FAILED') { if (!condition) { throw new ValidationError(message, code); } return true; } export { ValidationError, validateBuffer, validateBufferLength, validateNumberRange, validateHexString, validateAddress, validateBech32Address, validateBase58Address, validatePrivateKey, validatePublicKey, validateDerivationPath, validateNetwork, assertValid };