OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / amount-validator.cjs

159 lines · javascript ref:
/**
 * FILE: lib/amount-validator.cjs
 * STATUS: ACTIVE
 * RUNTIME: Bare
 * MODULE: CommonJS
 *
 * PURPOSE:
 * Amount validation with range checks and format validation
 *
 * USED BY:
 * - wallet-server.cjs (transaction validation)
 *
 * KEY EXPORTS:
 * - validateAmount(amountStr, chain) - Format and range validation
 * - validateSufficientBalance(amount, balance, fee, chain) - Balance check
 * - formatAmount(amount, chain) - Human-readable formatting
 *
 * VALIDATION:
 * - Checks for positive amounts
 * - Validates BigInt format (no decimals, scientific notation)
 * - Enforces maximum limits (21M BTC, 1B ETH)
 * - Balance sufficiency checks
 */

/**
 * Maximum supply limits (prevent overflow attacks)
 */
const MAX_BTC = 21_000_000n * 100_000_000n  // 21 million BTC in satoshi
const MAX_ETH = 1_000_000_000n * 1_000_000_000_000_000_000n  // 1 billion ETH in wei

/**
 * Validate amount string format and range
 * @param {string|number} amountStr - Amount in smallest unit (satoshi/wei)
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @returns {Object} { valid: boolean, amount?: bigint, reason?: string }
 */
function validateAmount(amountStr, chain) {
  // Check if amount is provided
  if (amountStr === undefined || amountStr === null || amountStr === '') {
    return { valid: false, reason: 'Amount is required' }
  }

  // Try to convert to BigInt
  let amount
  try {
    // Remove any spaces or underscores (common in large numbers)
    const cleaned = String(amountStr).replace(/[\s_]/g, '')

    // Check for scientific notation (not supported by BigInt)
    if (cleaned.includes('e') || cleaned.includes('E')) {
      return { valid: false, reason: 'Amount cannot be in scientific notation (use smallest unit: satoshi/wei)' }
    }

    // Check for decimal point (BigInt doesn't support decimals)
    if (cleaned.includes('.')) {
      return { valid: false, reason: 'Decimal values not supported (use smallest unit: satoshi/wei)' }
    }

    // Check for negative sign
    if (cleaned.includes('-')) {
      return { valid: false, reason: 'Amount cannot be negative' }
    }

    amount = BigInt(cleaned)
  } catch (err) {
    return { valid: false, reason: `Invalid amount format: ${err.message}` }
  }

  // Check if non-negative (allow zero for balance checks)
  if (amount < 0n) {
    return { valid: false, reason: 'Amount cannot be negative' }
  }

  // Check maximum limits (prevent overflow)
  if (chain === 'bitcoin' && amount > MAX_BTC) {
    return { valid: false, reason: 'Amount exceeds maximum (21M BTC = 2,100,000,000,000,000 satoshi)' }
  }
  if (chain === 'ethereum' && amount > MAX_ETH) {
    return { valid: false, reason: 'Amount exceeds maximum (1B ETH)' }
  }

  return { valid: true, amount }
}

/**
 * Validate sufficient balance for transaction
 * @param {bigint} amount - Amount to send
 * @param {bigint} balance - Available balance
 * @param {bigint} fee - Transaction fee
 * @param {string} chain - Blockchain identifier
 * @returns {Object} { valid: boolean, reason?: string }
 */
function validateSufficientBalance(amount, balance, fee, chain) {
  const total = amount + fee

  if (total > balance) {
    return {
      valid: false,
      reason: `Insufficient balance: need ${formatAmount(total, chain)}, have ${formatAmount(balance, chain)}`
    }
  }

  return { valid: true }
}

/**
 * Format amount for display (human-readable)
 * @param {bigint|string} amount - Amount in smallest unit
 * @param {string} chain - Blockchain identifier
 * @returns {string} Formatted amount with unit
 */
function formatAmount(amount, chain) {
  const amountBigInt = typeof amount === 'string' ? BigInt(amount) : amount

  if (chain === 'bitcoin') {
    // 1 BTC = 100,000,000 satoshi (1e8)
    const btc = Number(amountBigInt) / 1e8
    return `${btc.toFixed(8)} BTC`
  } else if (chain === 'ethereum') {
    // 1 ETH = 1,000,000,000,000,000,000 wei (1e18)
    const eth = Number(amountBigInt) / 1e18
    return `${eth.toFixed(18)} ETH`
  }

  return String(amountBigInt)
}

/**
 * Parse human-readable amount to smallest unit
 * @param {string} amountStr - Amount in BTC/ETH
 * @param {string} chain - Blockchain identifier
 * @returns {bigint} Amount in smallest unit (satoshi/wei)
 * @throws {Error} If amount is invalid
 */
function parseAmount(amountStr, chain) {
  const amount = parseFloat(amountStr)

  if (isNaN(amount) || amount < 0) {
    throw new Error('Invalid amount')
  }

  if (chain === 'bitcoin') {
    // Convert BTC to satoshi
    return BigInt(Math.floor(amount * 1e8))
  } else if (chain === 'ethereum') {
    // Convert ETH to wei
    return BigInt(Math.floor(amount * 1e18))
  }

  throw new Error(`Unsupported chain: ${chain}`)
}

module.exports = {
  validateAmount,
  validateSufficientBalance,
  formatAmount,
  parseAmount
}