SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
2.6 KB · 60 lines swift
Raw Blame History
1//2//  TokenAmount.swift3//  OS Vault4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation10import BigInt1112/// Integer-only conversion between base units and display strings.13/// `Double` is banned for money: 6- and 18-decimal tokens both exceed the14/// 53-bit mantissa long before real-world balances do.15public enum TokenAmount {1617    /// "12.5" with decimals 6 → 12_500_000. Returns nil on malformed input or18    /// more fraction digits than the token supports. Accepts "," as separator.19    public static func parse(_ input: String, decimals: Int) -> BigUInt? {20        let normalized = input21            .trimmingCharacters(in: .whitespacesAndNewlines)22            .replacingOccurrences(of: ",", with: ".")23        guard !normalized.isEmpty, normalized != "." else { return nil }2425        let parts = normalized.split(separator: ".", omittingEmptySubsequences: false)26        guard parts.count <= 2 else { return nil }2728        let wholePart = String(parts[0])29        let fracPart = parts.count == 2 ? String(parts[1]) : ""30        guard wholePart.allSatisfy(\.isNumber), fracPart.allSatisfy(\.isNumber) else { return nil }31        guard !(wholePart.isEmpty && fracPart.isEmpty) else { return nil }32        guard fracPart.count <= decimals else { return nil }3334        let whole = BigUInt(wholePart.isEmpty ? "0" : wholePart, radix: 10) ?? 035        let paddedFrac = fracPart.padding(toLength: decimals, withPad: "0", startingAt: 0)36        let frac = paddedFrac.isEmpty ? BigUInt(0) : (BigUInt(paddedFrac, radix: 10) ?? 0)37        return whole * BigUInt(10).power(decimals) + frac38    }3940    /// 12_500_000 with decimals 6 → "12.5". Trailing zeros trimmed,41    /// optionally capped to `maxFractionDigits` (truncated, never rounded up —42    /// a wallet must not display more than the user owns).43    public static func format(_ units: BigUInt, decimals: Int, maxFractionDigits: Int? = nil) -> String {44        let divisor = BigUInt(10).power(decimals)45        let whole = units / divisor46        var frac = String(units % divisor)47        frac = String(repeating: "0", count: max(0, decimals - frac.count)) + frac48        if let cap = maxFractionDigits, frac.count > cap {49            frac = String(frac.prefix(cap))50        }51        while frac.hasSuffix("0") { frac.removeLast() }52        return frac.isEmpty ? String(whole) : "\(whole).\(frac)"53    }5455    /// Wei → ETH display string (18 decimals), for gas costs.56    public static func formatWei(_ wei: BigUInt, maxFractionDigits: Int = 8) -> String {57        format(wei, decimals: 18, maxFractionDigits: maxFractionDigits)58    }59}60