const DECIMAL_RE = /^\d+(\.\d+)?$/; /** Parse a decimal string ("100.25") into integer base units. Rejects floats, negatives, exponents. */ export function parseDecimalToUnits(value: string, decimals: number): bigint { const trimmed = value.trim(); if (!DECIMAL_RE.test(trimmed)) { throw new Error(`invalid decimal amount: "${value}"`); } const [whole = '0', frac = ''] = trimmed.split('.'); if (frac.length > decimals) { throw new Error(`too many decimal places (max ${decimals}): "${value}"`); } return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, '0') || '0'); } /** Format integer base units back to a decimal string, trimming trailing zeros. */ export function formatUnits(units: bigint, decimals: number): string { const negative = units < 0n; const abs = negative ? -units : units; const digits = abs.toString().padStart(decimals + 1, '0'); const whole = digits.slice(0, digits.length - decimals); const frac = decimals > 0 ? digits.slice(digits.length - decimals).replace(/0+$/, '') : ''; return (negative ? '-' : '') + whole + (frac ? '.' + frac : ''); } /** Ceiling division for non-negative bigints. */ export function ceilDiv(numerator: bigint, denominator: bigint): bigint { if (denominator <= 0n) throw new Error('denominator must be positive'); if (numerator < 0n) throw new Error('numerator must be non-negative'); return (numerator + denominator - 1n) / denominator; } /** * The minimum confirmed amount that satisfies an option: * required × (1 − tolerance_bp/10000), rounded UP so a payment one base unit * below the true tolerance floor never qualifies. */ export function toleranceFloor(requiredAmount: bigint, toleranceBp: number): bigint { if (!Number.isInteger(toleranceBp) || toleranceBp < 0 || toleranceBp > 10000) { throw new Error(`tolerance_bp out of range: ${toleranceBp}`); } return ceilDiv(requiredAmount * BigInt(10000 - toleranceBp), 10000n); }