import Foundation /// Exact decimal <-> drops conversion (1 XRP = 1_000_000 drops) without /// floating point rounding errors. enum XRPAmount { static let dropsPerXRP: UInt64 = 1_000_000 static let maxDrops: UInt64 = 100_000_000_000 * dropsPerXRP // 1e11 XRP static func drops(fromXRP value: String) throws -> UInt64 { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty, !text.hasPrefix("-"), !text.hasPrefix("+") else { throw XRPError.rpc("Enter a valid amount") } let parts = text.split(separator: ".", omittingEmptySubsequences: false) guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") } let wholeText = parts[0].isEmpty ? "0" : String(parts[0]) guard let whole = UInt64(wholeText) else { throw XRPError.rpc("Enter a valid amount") } var fraction: UInt64 = 0 if parts.count == 2 { let fractionText = String(parts[1]) guard fractionText.count <= 6, fractionText.allSatisfy({ $0.isNumber }) else { throw XRPError.rpc("XRP supports at most 6 decimal places") } let padded = fractionText.padding(toLength: 6, withPad: "0", startingAt: 0) guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") } fraction = parsed } let (wholeDrops, overflow) = whole.multipliedReportingOverflow(by: dropsPerXRP) guard !overflow else { throw XRPError.rpc("Amount is too large") } let (total, carry) = wholeDrops.addingReportingOverflow(fraction) guard !carry, total <= maxDrops else { throw XRPError.rpc("Amount is too large") } return total } static func xrp(fromDrops drops: String) -> String { guard let value = UInt64(drops.trimmingCharacters(in: .whitespacesAndNewlines)) else { return "0" } return xrp(fromDrops: value) } static func xrp(fromDrops value: UInt64) -> String { let whole = value / dropsPerXRP let fraction = value % dropsPerXRP if fraction == 0 { return String(whole) } var fractionText = String(format: "%06llu", fraction) while fractionText.hasSuffix("0") { fractionText.removeLast() } return "\(whole).\(fractionText)" } }