taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 00e03ddb0cc3ae1192f312c0f547097e9eeb5d17
parent 416a1650f961fc496d825e547eefb499f8781fbd
Author: Jonathan Buchanan <jonathan.russ.buchanan@gmail.com>
Date:   Wed, 15 Jun 2022 14:09:48 -0400

separate common taler code into a local swift package

Diffstat:
MTaler.xcodeproj/project.pbxproj | 22++++++++++++++--------
MTaler.xcodeproj/project.xcworkspace/xcuserdata/jonathan.xcuserdatad/UserInterfaceState.xcuserstate | 0
DTaler/Amount.swift | 410-------------------------------------------------------------------------------
MTaler/AppDelegate.swift | 6+++++-
MTaler/BalanceList.swift | 4++--
MTaler/BalanceRow.swift | 1+
MTaler/WalletBackend.swift | 12++++--------
Mbootstrap | 5++---
Ataler-swift/.gitignore | 9+++++++++
Ataler-swift/Package.swift | 28++++++++++++++++++++++++++++
Ataler-swift/Sources/taler-swift/Amount.swift | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ataler-swift/Sources/taler-swift/taler_swift.swift | 6++++++
RTalerTests/AmountTests.swift -> taler-swift/Tests/taler-swiftTests/AmountTests.swift | 0
Ataler-swift/Tests/taler-swiftTests/taler_swiftTests.swift | 11+++++++++++
14 files changed, 492 insertions(+), 432 deletions(-)

diff --git a/Taler.xcodeproj/project.pbxproj b/Taler.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + ABC13AA32859962800D23185 /* taler-swift in Frameworks */ = {isa = PBXBuildFile; productRef = ABC13AA22859962800D23185 /* taler-swift */; }; D112510026B12E3200D02E00 /* taler-wallet-embedded.js in CopyFiles */ = {isa = PBXBuildFile; fileRef = D11250FF26B12E3200D02E00 /* taler-wallet-embedded.js */; }; - D1472E5526B9206800896566 /* AmountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1472E5426B9206800896566 /* AmountTests.swift */; }; D14AFD2124D232B300C51073 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD2024D232B300C51073 /* AppDelegate.swift */; }; D14AFD2324D232B300C51073 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD2224D232B300C51073 /* SceneDelegate.swift */; }; D14AFD3824D232B500C51073 /* TalerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD3724D232B500C51073 /* TalerTests.swift */; }; @@ -37,7 +37,6 @@ D17D8B8525ADB29B001BD43D /* libcares.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B4825ADB12B001BD43D /* libcares.a */; }; D18DBB5E26DF160D00A4480D /* TimestampTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18DBB5D26DF160D00A4480D /* TimestampTests.swift */; }; D1AFF0F3268D59C200FBB744 /* libiono.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1AFF0F2268D59A500FBB744 /* libiono.a */; }; - D1BA3F9226B8889600A5848B /* Amount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BA3F9126B8889600A5848B /* Amount.swift */; }; D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D65B9726992E4600C1012A /* WalletBackend.swift */; }; /* End PBXBuildFile section */ @@ -72,6 +71,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + AB710490285995B6008B04F0 /* taler-swift */ = {isa = PBXFileReference; lastKnownFileType = text; path = "taler-swift"; sourceTree = SOURCE_ROOT; }; D11250FF26B12E3200D02E00 /* taler-wallet-embedded.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "taler-wallet-embedded.js"; sourceTree = "<group>"; }; D11DB44E25A5C487009CF0BC /* libnode.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libnode.a; path = "nodejs-mobile/out/Release/libnode.a"; sourceTree = "<group>"; }; D11DB45625A5C5C7009CF0BC /* libv8_initializers.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libv8_initializers.a; path = "nodejs-mobile/out/Release/libv8_initializers.a"; sourceTree = "<group>"; }; @@ -115,7 +115,6 @@ D145D1EF25AC416B00CDD61B /* libv8_base_without_compiler.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libv8_base_without_compiler.a; path = "ios-node-v8/taler-ios-build/compiled/node-x64/libv8_base_without_compiler.a"; sourceTree = "<group>"; }; D145D1F025AC416B00CDD61B /* libhistogram.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libhistogram.a; path = "ios-node-v8/taler-ios-build/compiled/node-x64/libhistogram.a"; sourceTree = "<group>"; }; D145D1F125AC416B00CDD61B /* libv8_libbase.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libv8_libbase.a; path = "ios-node-v8/taler-ios-build/compiled/node-x64/libv8_libbase.a"; sourceTree = "<group>"; }; - D1472E5426B9206800896566 /* AmountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountTests.swift; sourceTree = "<group>"; }; D14AFD1D24D232B300C51073 /* Taler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Taler.app; sourceTree = BUILT_PRODUCTS_DIR; }; D14AFD2024D232B300C51073 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; D14AFD2224D232B300C51073 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; @@ -155,7 +154,6 @@ D18DBB5D26DF160D00A4480D /* TimestampTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampTests.swift; sourceTree = "<group>"; }; D1AB963B259EB13D00DEAB23 /* libnode.89.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libnode.89.dylib; path = "ios-node-v8/taler-ios-build/compiled/x64-v8a/libnode.89.dylib"; sourceTree = "<group>"; }; D1AFF0F2268D59A500FBB744 /* libiono.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libiono.a; path = iono/compiled/x64/libiono.a; sourceTree = "<group>"; }; - D1BA3F9126B8889600A5848B /* Amount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Amount.swift; sourceTree = "<group>"; }; D1D65B9726992E4600C1012A /* WalletBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBackend.swift; sourceTree = "<group>"; }; D1F0C22F25A958AE00C3179D /* libllhttp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libllhttp.a; path = "ios-node-v8/tools/ios-framework/bin/x64/libllhttp.a"; sourceTree = "<group>"; }; D1F0C23025A958AE00C3179D /* libv8_initializers.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libv8_initializers.a; path = "ios-node-v8/tools/ios-framework/bin/x64/libv8_initializers.a"; sourceTree = "<group>"; }; @@ -218,6 +216,7 @@ D17D8B7B25ADB29B001BD43D /* libv8_compiler.a in Frameworks */, D17D8B7225ADB29A001BD43D /* libbrotli.a in Frameworks */, D17D8B7625ADB29A001BD43D /* libv8_libsampler.a in Frameworks */, + ABC13AA32859962800D23185 /* taler-swift in Frameworks */, D17D8B7825ADB29B001BD43D /* libv8_libbase.a in Frameworks */, D17D8B7C25ADB29B001BD43D /* libv8_base_without_compiler.a in Frameworks */, D17D8B7725ADB29A001BD43D /* libv8_libplatform.a in Frameworks */, @@ -249,6 +248,7 @@ D14AFD1424D232B300C51073 = { isa = PBXGroup; children = ( + AB710490285995B6008B04F0 /* taler-swift */, D11250FF26B12E3200D02E00 /* taler-wallet-embedded.js */, D14AFD1F24D232B300C51073 /* Taler */, D14AFD3624D232B500C51073 /* TalerTests */, @@ -274,7 +274,6 @@ D14AFD2024D232B300C51073 /* AppDelegate.swift */, D14AFD2224D232B300C51073 /* SceneDelegate.swift */, D1D65B9726992E4600C1012A /* WalletBackend.swift */, - D1BA3F9126B8889600A5848B /* Amount.swift */, D14CE1B126C39E5D00612DBE /* BalanceRow.swift */, D14CE1B326C3A2D400612DBE /* BalanceList.swift */, D14AFD2624D232B500C51073 /* Assets.xcassets */, @@ -289,7 +288,6 @@ children = ( D14AFD3724D232B500C51073 /* TalerTests.swift */, D14AFD3924D232B500C51073 /* Info.plist */, - D1472E5426B9206800896566 /* AmountTests.swift */, D18DBB5D26DF160D00A4480D /* TimestampTests.swift */, ); path = TalerTests; @@ -436,6 +434,9 @@ dependencies = ( ); name = Taler; + packageProductDependencies = ( + ABC13AA22859962800D23185 /* taler-swift */, + ); productName = Taler; productReference = D14AFD1D24D232B300C51073 /* Taler.app */; productType = "com.apple.product-type.application"; @@ -591,7 +592,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D1BA3F9226B8889600A5848B /* Amount.swift in Sources */, D14AFD2124D232B300C51073 /* AppDelegate.swift in Sources */, D14AFD2324D232B300C51073 /* SceneDelegate.swift in Sources */, D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */, @@ -605,7 +605,6 @@ buildActionMask = 2147483647; files = ( D14AFD3824D232B500C51073 /* TalerTests.swift in Sources */, - D1472E5526B9206800896566 /* AmountTests.swift in Sources */, D18DBB5E26DF160D00A4480D /* TimestampTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -986,6 +985,13 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + ABC13AA22859962800D23185 /* taler-swift */ = { + isa = XCSwiftPackageProductDependency; + productName = "taler-swift"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D14AFD1524D232B300C51073 /* Project object */; } diff --git a/Taler.xcodeproj/project.xcworkspace/xcuserdata/jonathan.xcuserdatad/UserInterfaceState.xcuserstate b/Taler.xcodeproj/project.xcworkspace/xcuserdata/jonathan.xcuserdatad/UserInterfaceState.xcuserstate Binary files differ. diff --git a/Taler/Amount.swift b/Taler/Amount.swift @@ -1,410 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2021 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import Foundation - -/** - Errors for `Amount`. - */ -enum AmountError: Error { - /** - The string cannot be parsed to create an `Amount`. - */ - case invalidStringRepresentation - - /** - Could not compare or operate on two `Amount`s of different currencies. - */ - case incompatibleCurrency - - /** - The amount is invalid. The value is either greater than the maximum, or the currency is the empty string. - */ - case invalidAmount - - /** - The result of the operation would yield a negative amount. - */ - case negativeAmount - - /** - The operation was division by zero. - */ - case divideByZero -} - -/** - A value of a currency. - */ -class Amount: Codable, CustomStringConvertible { - /** - The largest possible value that can be represented. - */ - private static let maxValue: UInt64 = 1 << 52 - - /** - The size of `value` in relation to `fraction`. - */ - private static let fractionalBase: UInt32 = 100000000 - - /** - The greatest number of decimal digits that can be represented. - */ - private static let fractionalBaseDigits: UInt = 8 - - /** - The currency of the amount. - */ - var currency: String - - /** - The value of the amount (number to the left of the decimal point). - */ - var value: UInt64 - - /** - The fractional value of the amount (number to the right of the decimal point). - */ - var fraction: UInt32 - - /** - The string representation of the amount, formatted as "`currency`:`value`.`fraction`". - */ - var description: String { - if fraction == 0 { - return "\(currency):\(value)" - } else { - var frac = fraction - var fracStr = "" - while (frac > 0) { - fracStr += "\(frac / (Amount.fractionalBase / 10))" - frac = (frac * 10) % Amount.fractionalBase - } - return "\(currency):\(value).\(fracStr)" - } - } - - /** - Whether the value is valid. An amount is valid if and only if the currency is not empty and the value is less than the maximum allowed value. - */ - var valid: Bool { - return (value <= Amount.maxValue && currency != "") - } - - /** - Initializes an amount by parsing a string representing the amount. The string should be formatted as "`currency`:`value`.`fraction`". - - - Parameters: - - fromString: The string to parse. - - - Throws: - - `AmountError.invalidStringRepresentation` if the string cannot be parsed. - - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. - */ - init(fromString string: String) throws { - guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } - self.currency = String(string[..<separatorIndex]) - let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...]) - if let dotIndex = amountStr.firstIndex(of: ".") { - let valueStr = String(amountStr[..<dotIndex]) - let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...]) - guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value - self.fraction = 0 - var digitValue = Amount.fractionalBase / 10 - for char in fractionStr { - guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation } - self.fraction += digitValue * UInt32(digit) - digitValue /= 10 - } - } else { - guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value - self.fraction = 0 - } - guard self.valid else { throw AmountError.invalidAmount } - } - - /** - Initializes an amount with the specified currency, value, and fraction. - - - Parameters: - - currency: The currency of the amount. - - value: The value of the amount (number to the left of the decimal point). - - fraction: The fractional value of the amount (number to the right of the decimal point). - */ - init(currency: String, value: UInt64, fraction: UInt32) { - self.currency = currency - self.value = value - self.fraction = fraction - } - - /** - Initializes an amount from a decoder. - - - Parameters: - - fromDecoder: The decoder to extract the amount from. - - - Throws: - - `AmountError.invalidStringRepresentation` if the string cannot be parsed. - - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. - */ - init(fromDecoder decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - /* TODO: de-duplicate */ - guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } - self.currency = String(string[..<separatorIndex]) - let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...]) - if let dotIndex = amountStr.firstIndex(of: ".") { - let valueStr = String(amountStr[..<dotIndex]) - let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...]) - guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value - self.fraction = 0 - var digitValue = Amount.fractionalBase / 10 - for char in fractionStr { - guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation } - self.fraction += digitValue * UInt32(digit) - digitValue /= 10 - } - } else { - guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value - self.fraction = 0 - } - guard self.valid else { throw AmountError.invalidAmount } - } - - /** - Copies an amount. - - - Returns: A copy of the amount. - */ - func copy() -> Amount { - return Amount(currency: self.currency, value: self.value, fraction: self.fraction) - } - - /** - Creates a normalized copy of an amount. - - - Returns: A copy of the amount that has been normalized - */ - func normalizedCopy() throws -> Amount { - let amount = self.copy() - try amount.normalize() - return amount - } - - /** - Encodes an amount. - - - Parameters: - - to: The encoder to encode the amount with. - */ - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.description) - } - - /** - Normalizes an amount by reducing `fraction` until it is less than `Amount.fractionalBase`, increasing `value` appropriately. - - - Throws: - - `AmountError.invalidAmount` if the amount is invalid either before or after normalization. - */ - func normalize() throws { - if !valid { - throw AmountError.invalidAmount - } - self.value += UInt64(self.fraction / Amount.fractionalBase) - self.fraction = self.fraction % Amount.fractionalBase - if !valid { - throw AmountError.invalidAmount - } - } - - /** - Adds two amounts together. - - - Parameters: - - left: The amount on the left. - - right: The amount on the right. - - - Throws: - - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. - - - Returns: The sum of `left` and `right`, normalized. - */ - static func + (left: Amount, right: Amount) throws -> Amount { - if left.currency != right.currency { - throw AmountError.incompatibleCurrency - } - let leftNormalized = try left.normalizedCopy() - let rightNormalized = try right.normalizedCopy() - let result: Amount = leftNormalized - result.value += rightNormalized.value - result.fraction += rightNormalized.fraction - try result.normalize() - return result - } - - /** - Subtracts one amount from another. - - - Parameters: - - left: The amount on the left. - - right: The amount on the right. - - - Throws: - - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. - - - Returns: The difference of `left` and `right`, normalized. - */ - static func - (left: Amount, right: Amount) throws -> Amount { - if left.currency != right.currency { - throw AmountError.incompatibleCurrency - } - let leftNormalized = try left.normalizedCopy() - let rightNormalized = try right.normalizedCopy() - if (leftNormalized.fraction < rightNormalized.fraction) { - guard leftNormalized.value != 0 else { throw AmountError.negativeAmount } - leftNormalized.value -= 1 - leftNormalized.fraction += Amount.fractionalBase - } - guard leftNormalized.value >= rightNormalized.value else { throw AmountError.negativeAmount } - let diff = Amount.zero(currency: left.currency) - diff.value = leftNormalized.value - rightNormalized.value - diff.fraction = leftNormalized.fraction - rightNormalized.fraction - try diff.normalize() - return diff - } - - /** - Divides an amount by a scalar, possibly introducing rounding error. - - - Parameters: - - dividend: The amount to divide. - - divisor: The scalar dividing `dividend`. - - - Returns: The quotient of `dividend` and `divisor`, normalized. - */ - static func / (dividend: Amount, divisor: UInt32) throws -> Amount { - guard divisor != 0 else { throw AmountError.divideByZero } - let result = try dividend.normalizedCopy() - if (divisor == 1) { - return result - } - var remainder = result.value % UInt64(divisor) - result.value = result.value / UInt64(divisor) - remainder = (remainder * UInt64(Amount.fractionalBase)) + UInt64(result.fraction) - result.fraction = UInt32(remainder) / divisor - try result.normalize() - return result - } - - /** - Multiply an amount by a scalar. - - - Parameters: - - amount: The amount to multiply. - - factor: The scalar multiplying `amount`. - - - Returns: The product of `amount` and `factor`, normalized. - */ - static func * (amount: Amount, factor: UInt32) throws -> Amount { - let result = try amount.normalizedCopy() - result.value = result.value * UInt64(factor) - let fraction_tmp = UInt64(result.fraction) * UInt64(factor) - result.value += fraction_tmp / UInt64(Amount.fractionalBase) - result.fraction = UInt32(fraction_tmp % UInt64(Amount.fractionalBase)) - return result - } - - /** - Compares two amounts. - - - Parameters: - - left: The first amount. - - right: The second amount. - - - Throws: - - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. - - - Returns: `true` if and only if the amounts have the same `value` and `fraction` after normalization, `false` otherwise. - */ - static func == (left: Amount, right: Amount) throws -> Bool { - if left.currency != right.currency { - throw AmountError.incompatibleCurrency - } - let leftNormalized = try left.normalizedCopy() - let rightNormalized = try right.normalizedCopy() - return (leftNormalized.value == rightNormalized.value && leftNormalized.fraction == rightNormalized.fraction) - } - - /** - Compares two amounts. - - - Parameters: - - left: The amount on the left. - - right: The amount on the right. - - - Throws: - - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. - - - Returns: `true` if and only if `left` is smaller than `right` after normalization, `false` otherwise. - */ - static func < (left: Amount, right: Amount) throws -> Bool { - if left.currency != right.currency { - throw AmountError.incompatibleCurrency - } - let leftNormalized = try left.normalizedCopy() - let rightNormalized = try right.normalizedCopy() - if (leftNormalized.value == rightNormalized.value) { - return (leftNormalized.fraction < rightNormalized.fraction) - } else { - return (leftNormalized.value < rightNormalized.value) - } - } - - /** - Compares two amounts. - - - Parameters: - - left: The amount on the left. - - right: The amount on the right. - - - Throws: - - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. - - - Returns: `true` if and only if `left` is bigger than `right` after normalization, `false` otherwise. - */ - static func > (left: Amount, right: Amount) throws -> Bool { - return try right < left - } - - /** - Creates the amount representing zero in a given currency. - - - Parameters: - - currency: The currency to use. - - - Returns: The zero amount for `currency`. - */ - static func zero(currency: String) -> Amount { - return Amount(currency: currency, value: 0, fraction: 0) - } -} diff --git a/Taler/AppDelegate.swift b/Taler/AppDelegate.swift @@ -24,7 +24,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - let backend = WalletBackend() + do { + let backend = try WalletBackend() + } catch { + + } return true } diff --git a/Taler/BalanceList.swift b/Taler/BalanceList.swift @@ -16,7 +16,7 @@ import SwiftUI -struct IdentifiedArray<T>: RandomAccessCollection { +/*struct IdentifiedArray<T>: RandomAccessCollection { struct Item { var id: Int var item: T @@ -73,4 +73,4 @@ struct BalanceList_Previews: PreviewProvider { Balance(available: Amount(fromString: "EUR:0.02"), pendingIncoming: Amount(fromString: "EUR:0.01"), pendingOutgoing: Amount(fromString: "EUR:0.03"), requiresUserInput: false) ])) } -} +}*/ diff --git a/Taler/BalanceRow.swift b/Taler/BalanceRow.swift @@ -15,6 +15,7 @@ */ import SwiftUI +import taler_swift struct BalanceRow: View { var balance: Balance diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift @@ -16,6 +16,7 @@ import Foundation import iono +import taler_swift enum WalletBackendResponseError: Error { case malformedResponse @@ -1304,6 +1305,7 @@ class WalletBackendSuspendCoinRequest: WalletBackendRequest { } enum WalletBackendError: Error { + case initializationError case serializationError case deserializationError } @@ -1319,7 +1321,7 @@ class WalletBackend: IonoMessageHandler { } private var requests: [UInt : RequestDetail] = [:] - init() { + init() throws { iono = Iono() requestsMade = 0 self.backendReady = false @@ -1335,7 +1337,7 @@ class WalletBackend: IonoMessageHandler { iono.evalNodeCode(source: "tw = require('@gnu-taler/taler-wallet-embedded');") iono.evalNodeCode(source: "tw.installNativeWalletListener();") } catch { - + throw WalletBackendError.initializationError } // Send the init message @@ -1351,12 +1353,6 @@ class WalletBackend: IonoMessageHandler { } waitUntilReady() - //try! sendRequest(request: WalletBackendWithdrawTestBalance(amount: "TESTKUDOS:10", bankBaseUrl: "https://bank.test.taler.net/", exchangeBaseUrl: "https://exchange.test.taler.net/")) - try! sendRequest(request: WalletBackendGetBalancesRequest(onSuccess: { ([Balance]) -> Void in - - }, onFailure: { () -> Void in - - })) } func waitUntilReady() { diff --git a/bootstrap b/bootstrap @@ -1,10 +1,9 @@ #!/bin/sh -# Bootstrap the repository. Used when the repository is checked out from git. -# When using the source tarball, running this script is not necessary. - set -eu +curl https://git.taler.net/wallet-core.git/plain/v0.9.0-dev.12/taler-wallet-embedded.js?h=prebuilt --output taler-wallet-embedded.js + if ! git --version >/dev/null; then echo "git not installed" exit 1 diff --git a/taler-swift/.gitignore b/taler-swift/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/taler-swift/Package.swift b/taler-swift/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "taler-swift", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "taler-swift", + targets: ["taler-swift"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "taler-swift", + dependencies: []), + .testTarget( + name: "taler-swiftTests", + dependencies: ["taler-swift"]), + ] +) diff --git a/taler-swift/Sources/taler-swift/Amount.swift b/taler-swift/Sources/taler-swift/Amount.swift @@ -0,0 +1,410 @@ +/* + * This file is part of GNU Taler + * (C) 2021 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import Foundation + +/** + Errors for `Amount`. + */ +enum AmountError: Error { + /** + The string cannot be parsed to create an `Amount`. + */ + case invalidStringRepresentation + + /** + Could not compare or operate on two `Amount`s of different currencies. + */ + case incompatibleCurrency + + /** + The amount is invalid. The value is either greater than the maximum, or the currency is the empty string. + */ + case invalidAmount + + /** + The result of the operation would yield a negative amount. + */ + case negativeAmount + + /** + The operation was division by zero. + */ + case divideByZero +} + +/** + A value of a currency. + */ +public class Amount: Codable, CustomStringConvertible { + /** + The largest possible value that can be represented. + */ + private static let maxValue: UInt64 = 1 << 52 + + /** + The size of `value` in relation to `fraction`. + */ + private static let fractionalBase: UInt32 = 100000000 + + /** + The greatest number of decimal digits that can be represented. + */ + private static let fractionalBaseDigits: UInt = 8 + + /** + The currency of the amount. + */ + var currency: String + + /** + The value of the amount (number to the left of the decimal point). + */ + var value: UInt64 + + /** + The fractional value of the amount (number to the right of the decimal point). + */ + var fraction: UInt32 + + /** + The string representation of the amount, formatted as "`currency`:`value`.`fraction`". + */ + public var description: String { + if fraction == 0 { + return "\(currency):\(value)" + } else { + var frac = fraction + var fracStr = "" + while (frac > 0) { + fracStr += "\(frac / (Amount.fractionalBase / 10))" + frac = (frac * 10) % Amount.fractionalBase + } + return "\(currency):\(value).\(fracStr)" + } + } + + /** + Whether the value is valid. An amount is valid if and only if the currency is not empty and the value is less than the maximum allowed value. + */ + var valid: Bool { + return (value <= Amount.maxValue && currency != "") + } + + /** + Initializes an amount by parsing a string representing the amount. The string should be formatted as "`currency`:`value`.`fraction`". + + - Parameters: + - fromString: The string to parse. + + - Throws: + - `AmountError.invalidStringRepresentation` if the string cannot be parsed. + - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. + */ + public init(fromString string: String) throws { + guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } + self.currency = String(string[..<separatorIndex]) + let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...]) + if let dotIndex = amountStr.firstIndex(of: ".") { + let valueStr = String(amountStr[..<dotIndex]) + let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...]) + guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation } + self.value = _value + self.fraction = 0 + var digitValue = Amount.fractionalBase / 10 + for char in fractionStr { + guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation } + self.fraction += digitValue * UInt32(digit) + digitValue /= 10 + } + } else { + guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } + self.value = _value + self.fraction = 0 + } + guard self.valid else { throw AmountError.invalidAmount } + } + + /** + Initializes an amount with the specified currency, value, and fraction. + + - Parameters: + - currency: The currency of the amount. + - value: The value of the amount (number to the left of the decimal point). + - fraction: The fractional value of the amount (number to the right of the decimal point). + */ + init(currency: String, value: UInt64, fraction: UInt32) { + self.currency = currency + self.value = value + self.fraction = fraction + } + + /** + Initializes an amount from a decoder. + + - Parameters: + - fromDecoder: The decoder to extract the amount from. + + - Throws: + - `AmountError.invalidStringRepresentation` if the string cannot be parsed. + - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. + */ + init(fromDecoder decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + /* TODO: de-duplicate */ + guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } + self.currency = String(string[..<separatorIndex]) + let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...]) + if let dotIndex = amountStr.firstIndex(of: ".") { + let valueStr = String(amountStr[..<dotIndex]) + let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...]) + guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation } + self.value = _value + self.fraction = 0 + var digitValue = Amount.fractionalBase / 10 + for char in fractionStr { + guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation } + self.fraction += digitValue * UInt32(digit) + digitValue /= 10 + } + } else { + guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } + self.value = _value + self.fraction = 0 + } + guard self.valid else { throw AmountError.invalidAmount } + } + + /** + Copies an amount. + + - Returns: A copy of the amount. + */ + func copy() -> Amount { + return Amount(currency: self.currency, value: self.value, fraction: self.fraction) + } + + /** + Creates a normalized copy of an amount. + + - Returns: A copy of the amount that has been normalized + */ + func normalizedCopy() throws -> Amount { + let amount = self.copy() + try amount.normalize() + return amount + } + + /** + Encodes an amount. + + - Parameters: + - to: The encoder to encode the amount with. + */ + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } + + /** + Normalizes an amount by reducing `fraction` until it is less than `Amount.fractionalBase`, increasing `value` appropriately. + + - Throws: + - `AmountError.invalidAmount` if the amount is invalid either before or after normalization. + */ + func normalize() throws { + if !valid { + throw AmountError.invalidAmount + } + self.value += UInt64(self.fraction / Amount.fractionalBase) + self.fraction = self.fraction % Amount.fractionalBase + if !valid { + throw AmountError.invalidAmount + } + } + + /** + Adds two amounts together. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: The sum of `left` and `right`, normalized. + */ + static func + (left: Amount, right: Amount) throws -> Amount { + if left.currency != right.currency { + throw AmountError.incompatibleCurrency + } + let leftNormalized = try left.normalizedCopy() + let rightNormalized = try right.normalizedCopy() + let result: Amount = leftNormalized + result.value += rightNormalized.value + result.fraction += rightNormalized.fraction + try result.normalize() + return result + } + + /** + Subtracts one amount from another. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: The difference of `left` and `right`, normalized. + */ + static func - (left: Amount, right: Amount) throws -> Amount { + if left.currency != right.currency { + throw AmountError.incompatibleCurrency + } + let leftNormalized = try left.normalizedCopy() + let rightNormalized = try right.normalizedCopy() + if (leftNormalized.fraction < rightNormalized.fraction) { + guard leftNormalized.value != 0 else { throw AmountError.negativeAmount } + leftNormalized.value -= 1 + leftNormalized.fraction += Amount.fractionalBase + } + guard leftNormalized.value >= rightNormalized.value else { throw AmountError.negativeAmount } + let diff = Amount.zero(currency: left.currency) + diff.value = leftNormalized.value - rightNormalized.value + diff.fraction = leftNormalized.fraction - rightNormalized.fraction + try diff.normalize() + return diff + } + + /** + Divides an amount by a scalar, possibly introducing rounding error. + + - Parameters: + - dividend: The amount to divide. + - divisor: The scalar dividing `dividend`. + + - Returns: The quotient of `dividend` and `divisor`, normalized. + */ + static func / (dividend: Amount, divisor: UInt32) throws -> Amount { + guard divisor != 0 else { throw AmountError.divideByZero } + let result = try dividend.normalizedCopy() + if (divisor == 1) { + return result + } + var remainder = result.value % UInt64(divisor) + result.value = result.value / UInt64(divisor) + remainder = (remainder * UInt64(Amount.fractionalBase)) + UInt64(result.fraction) + result.fraction = UInt32(remainder) / divisor + try result.normalize() + return result + } + + /** + Multiply an amount by a scalar. + + - Parameters: + - amount: The amount to multiply. + - factor: The scalar multiplying `amount`. + + - Returns: The product of `amount` and `factor`, normalized. + */ + static func * (amount: Amount, factor: UInt32) throws -> Amount { + let result = try amount.normalizedCopy() + result.value = result.value * UInt64(factor) + let fraction_tmp = UInt64(result.fraction) * UInt64(factor) + result.value += fraction_tmp / UInt64(Amount.fractionalBase) + result.fraction = UInt32(fraction_tmp % UInt64(Amount.fractionalBase)) + return result + } + + /** + Compares two amounts. + + - Parameters: + - left: The first amount. + - right: The second amount. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if the amounts have the same `value` and `fraction` after normalization, `false` otherwise. + */ + static func == (left: Amount, right: Amount) throws -> Bool { + if left.currency != right.currency { + throw AmountError.incompatibleCurrency + } + let leftNormalized = try left.normalizedCopy() + let rightNormalized = try right.normalizedCopy() + return (leftNormalized.value == rightNormalized.value && leftNormalized.fraction == rightNormalized.fraction) + } + + /** + Compares two amounts. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if `left` is smaller than `right` after normalization, `false` otherwise. + */ + static func < (left: Amount, right: Amount) throws -> Bool { + if left.currency != right.currency { + throw AmountError.incompatibleCurrency + } + let leftNormalized = try left.normalizedCopy() + let rightNormalized = try right.normalizedCopy() + if (leftNormalized.value == rightNormalized.value) { + return (leftNormalized.fraction < rightNormalized.fraction) + } else { + return (leftNormalized.value < rightNormalized.value) + } + } + + /** + Compares two amounts. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if `left` is bigger than `right` after normalization, `false` otherwise. + */ + static func > (left: Amount, right: Amount) throws -> Bool { + return try right < left + } + + /** + Creates the amount representing zero in a given currency. + + - Parameters: + - currency: The currency to use. + + - Returns: The zero amount for `currency`. + */ + static func zero(currency: String) -> Amount { + return Amount(currency: currency, value: 0, fraction: 0) + } +} diff --git a/taler-swift/Sources/taler-swift/taler_swift.swift b/taler-swift/Sources/taler-swift/taler_swift.swift @@ -0,0 +1,6 @@ +public struct taler_swift { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/TalerTests/AmountTests.swift b/taler-swift/Tests/taler-swiftTests/AmountTests.swift diff --git a/taler-swift/Tests/taler-swiftTests/taler_swiftTests.swift b/taler-swift/Tests/taler-swiftTests/taler_swiftTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import taler_swift + +final class taler_swiftTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(taler_swift().text, "Hello, World!") + } +}