// // TokenAmount.swift // OS Vault // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import Foundation import BigInt /// Integer-only conversion between base units and display strings. /// `Double` is banned for money: 6- and 18-decimal tokens both exceed the /// 53-bit mantissa long before real-world balances do. public enum TokenAmount { /// "12.5" with decimals 6 → 12_500_000. Returns nil on malformed input or /// more fraction digits than the token supports. Accepts "," as separator. public static func parse(_ input: String, decimals: Int) -> BigUInt? { let normalized = input .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: ",", with: ".") guard !normalized.isEmpty, normalized != "." else { return nil } let parts = normalized.split(separator: ".", omittingEmptySubsequences: false) guard parts.count <= 2 else { return nil } let wholePart = String(parts[0]) let fracPart = parts.count == 2 ? String(parts[1]) : "" guard wholePart.allSatisfy(\.isNumber), fracPart.allSatisfy(\.isNumber) else { return nil } guard !(wholePart.isEmpty && fracPart.isEmpty) else { return nil } guard fracPart.count <= decimals else { return nil } let whole = BigUInt(wholePart.isEmpty ? "0" : wholePart, radix: 10) ?? 0 let paddedFrac = fracPart.padding(toLength: decimals, withPad: "0", startingAt: 0) let frac = paddedFrac.isEmpty ? BigUInt(0) : (BigUInt(paddedFrac, radix: 10) ?? 0) return whole * BigUInt(10).power(decimals) + frac } /// 12_500_000 with decimals 6 → "12.5". Trailing zeros trimmed, /// optionally capped to `maxFractionDigits` (truncated, never rounded up — /// a wallet must not display more than the user owns). public static func format(_ units: BigUInt, decimals: Int, maxFractionDigits: Int? = nil) -> String { let divisor = BigUInt(10).power(decimals) let whole = units / divisor var frac = String(units % divisor) frac = String(repeating: "0", count: max(0, decimals - frac.count)) + frac if let cap = maxFractionDigits, frac.count > cap { frac = String(frac.prefix(cap)) } while frac.hasSuffix("0") { frac.removeLast() } return frac.isEmpty ? String(whole) : "\(whole).\(frac)" } /// Wei → ETH display string (18 decimals), for gas costs. public static func formatWei(_ wei: BigUInt, maxFractionDigits: Int = 8) -> String { format(wei, decimals: 18, maxFractionDigits: maxFractionDigits) } }