/**
* 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
}