diff options
-rw-r--r-- | Taler.xcodeproj/project.pbxproj | 16 | ||||
-rw-r--r-- | Taler/BalanceList.swift | 76 | ||||
-rw-r--r-- | Taler/BalanceRow.swift (renamed from Taler/ContentView.swift) | 16 | ||||
-rw-r--r-- | Taler/SceneDelegate.swift | 9 | ||||
-rw-r--r-- | Taler/WalletBackend.swift | 1168 | ||||
-rw-r--r-- | TalerTests/TimestampTests.swift | 43 |
6 files changed, 1289 insertions, 39 deletions
diff --git a/Taler.xcodeproj/project.pbxproj b/Taler.xcodeproj/project.pbxproj index 12f8c85..0943e6f 100644 --- a/Taler.xcodeproj/project.pbxproj +++ b/Taler.xcodeproj/project.pbxproj @@ -11,9 +11,10 @@ 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 */; }; - D14AFD2524D232B300C51073 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD2424D232B300C51073 /* ContentView.swift */; }; D14AFD3824D232B500C51073 /* TalerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD3724D232B500C51073 /* TalerTests.swift */; }; D14AFD4324D232B500C51073 /* TalerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD4224D232B500C51073 /* TalerUITests.swift */; }; + D14CE1B226C39E5D00612DBE /* BalanceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14CE1B126C39E5D00612DBE /* BalanceRow.swift */; }; + D14CE1B426C3A2D400612DBE /* BalanceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14CE1B326C3A2D400612DBE /* BalanceList.swift */; }; D17D8B7225ADB29A001BD43D /* libbrotli.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B4F25ADB12D001BD43D /* libbrotli.a */; }; D17D8B7325ADB29A001BD43D /* libzlib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B4725ADB12B001BD43D /* libzlib.a */; }; D17D8B7425ADB29A001BD43D /* libv8_zlib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B5425ADB12D001BD43D /* libv8_zlib.a */; }; @@ -34,6 +35,7 @@ D17D8B8325ADB29B001BD43D /* libllhttp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B4D25ADB12C001BD43D /* libllhttp.a */; }; D17D8B8425ADB29B001BD43D /* libhistogram.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D17D8B5625ADB130001BD43D /* libhistogram.a */; }; 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 */; }; @@ -117,7 +119,6 @@ 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>"; }; - D14AFD2424D232B300C51073 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; D14AFD2624D232B500C51073 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; D14AFD2C24D232B500C51073 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; D14AFD2E24D232B500C51073 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; @@ -127,6 +128,8 @@ D14AFD3E24D232B500C51073 /* TalerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TalerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D14AFD4224D232B500C51073 /* TalerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalerUITests.swift; sourceTree = "<group>"; }; D14AFD4424D232B500C51073 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + D14CE1B126C39E5D00612DBE /* BalanceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceRow.swift; sourceTree = "<group>"; }; + D14CE1B326C3A2D400612DBE /* BalanceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceList.swift; sourceTree = "<group>"; }; D1595BBD25A550750049971F /* libnode.89.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libnode.89.dylib; path = "ios-node-v8/out/Debug/libnode.89.dylib"; sourceTree = "<group>"; }; D1595BC625A5527C0049971F /* NodeMobile.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NodeMobile.framework; path = "nodejs-mobile-v0.3.2-ios/Release-universal/NodeMobile.framework"; sourceTree = "<group>"; }; D17D8B4425ADB12B001BD43D /* libv8_libbase.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libv8_libbase.a; path = "ios-node-v8/taler-ios-build/compiled/node-arm64/libv8_libbase.a"; sourceTree = "<group>"; }; @@ -149,6 +152,7 @@ D17D8B5525ADB12E001BD43D /* libuvwasi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuvwasi.a; path = "ios-node-v8/taler-ios-build/compiled/node-arm64/libuvwasi.a"; sourceTree = "<group>"; }; D17D8B5625ADB130001BD43D /* libhistogram.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libhistogram.a; path = "ios-node-v8/taler-ios-build/compiled/node-arm64/libhistogram.a"; sourceTree = "<group>"; }; D17D8B5725ADB130001BD43D /* libtorque_base.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtorque_base.a; path = "ios-node-v8/taler-ios-build/compiled/node-arm64/libtorque_base.a"; sourceTree = "<group>"; }; + 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>"; }; @@ -271,7 +275,8 @@ D14AFD2224D232B300C51073 /* SceneDelegate.swift */, D1D65B9726992E4600C1012A /* WalletBackend.swift */, D1BA3F9126B8889600A5848B /* Amount.swift */, - D14AFD2424D232B300C51073 /* ContentView.swift */, + D14CE1B126C39E5D00612DBE /* BalanceRow.swift */, + D14CE1B326C3A2D400612DBE /* BalanceList.swift */, D14AFD2624D232B500C51073 /* Assets.xcassets */, D14AFD2B24D232B500C51073 /* LaunchScreen.storyboard */, D14AFD2E24D232B500C51073 /* Info.plist */, @@ -285,6 +290,7 @@ D14AFD3724D232B500C51073 /* TalerTests.swift */, D14AFD3924D232B500C51073 /* Info.plist */, D1472E5426B9206800896566 /* AmountTests.swift */, + D18DBB5D26DF160D00A4480D /* TimestampTests.swift */, ); path = TalerTests; sourceTree = "<group>"; @@ -588,8 +594,9 @@ D1BA3F9226B8889600A5848B /* Amount.swift in Sources */, D14AFD2124D232B300C51073 /* AppDelegate.swift in Sources */, D14AFD2324D232B300C51073 /* SceneDelegate.swift in Sources */, - D14AFD2524D232B300C51073 /* ContentView.swift in Sources */, D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */, + D14CE1B426C3A2D400612DBE /* BalanceList.swift in Sources */, + D14CE1B226C39E5D00612DBE /* BalanceRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -599,6 +606,7 @@ files = ( D14AFD3824D232B500C51073 /* TalerTests.swift in Sources */, D1472E5526B9206800896566 /* AmountTests.swift in Sources */, + D18DBB5E26DF160D00A4480D /* TimestampTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Taler/BalanceList.swift b/Taler/BalanceList.swift new file mode 100644 index 0000000..5e77b6c --- /dev/null +++ b/Taler/BalanceList.swift @@ -0,0 +1,76 @@ +/* + * 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 SwiftUI + +struct IdentifiedArray<T>: RandomAccessCollection { + struct Item { + var id: Int + var item: T + } + + typealias Element = Item + typealias ItemArray = [Item] + typealias Index = ItemArray.Index + typealias SubSequence = IdentifiedArray<T> + typealias Indices = ItemArray.Indices + + var items: ItemArray + var startIndex: ItemArray.Index { + get { + return items.startIndex + } + } + var endIndex: ItemArray.Index { + get { + return items.endIndex + } + } + + subscript(position: ItemArray.Index) -> Element { + get { + return items[position] + } + set(value) { + items[position] = value + } + } + + init(_ array: [T]) { + self.items = array.enumerated().map({ (index, element) in + return Item(id: index, item: element) + }) + } +} + +struct BalanceList: View { + var balances: IdentifiedArray<Balance> + + var body: some View { + List(balances, id: \.id) { balance in + BalanceRow(balance: balance.item) + } + } +} + +struct BalanceList_Previews: PreviewProvider { + static var previews: some View { + try! BalanceList(balances: IdentifiedArray<Balance>([ + Balance(available: Amount(fromString: "USD:0.01"), pendingIncoming: Amount(fromString: "USD:0.02"), pendingOutgoing: Amount(fromString: "USD:0.03"), requiresUserInput: true), + 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/ContentView.swift b/Taler/BalanceRow.swift index 4406fc3..405a141 100644 --- a/Taler/ContentView.swift +++ b/Taler/BalanceRow.swift @@ -16,14 +16,22 @@ import SwiftUI -struct ContentView: View { +struct BalanceRow: View { + var balance: Balance + var body: some View { - Text("Hello, World!") + VStack(alignment: .leading, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/, content: { + Text("Available: \(balance.available.description)") + Text("Pending Incoming: \(balance.pendingIncoming.description)") + Text("Pending Outgoing: \(balance.pendingOutgoing.description)") + Text("Requires User Input: \(balance.requiresUserInput.description)") + }) + .padding() } } -struct ContentView_Previews: PreviewProvider { +struct BalanceRow_Previews: PreviewProvider { static var previews: some View { - ContentView() + try! BalanceRow(balance: Balance(available: Amount(fromString: "USD:0.01"), pendingIncoming: Amount(fromString: "USD:0.02"), pendingOutgoing: Amount(fromString: "USD:0.03"), requiresUserInput: true)) } } diff --git a/Taler/SceneDelegate.swift b/Taler/SceneDelegate.swift index a895f65..79b086e 100644 --- a/Taler/SceneDelegate.swift +++ b/Taler/SceneDelegate.swift @@ -27,15 +27,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() - // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) + /*let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: /* */) self.window = window - window.makeKeyAndVisible() + window.makeKeyAndVisible()*/ } } diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift index d5cc3ab..f1b462f 100644 --- a/Taler/WalletBackend.swift +++ b/Taler/WalletBackend.swift @@ -83,20 +83,40 @@ fileprivate struct WalletBackendInitRequest: WalletBackendRequest { } } -fileprivate struct WalletBackendGetTransactionsRequest: WalletBackendRequest { +/** + An balance on a wallet. + */ +struct Balance: Decodable { + var available: Amount + var pendingIncoming: Amount + var pendingOutgoing: Amount + var requiresUserInput: Bool +} + +/** + A request to get the balances held in the wallet. + */ +class WalletBackendGetBalancesRequest: WalletBackendRequest { + struct BalancesResponse: Decodable { + var balances: [Balance] + } struct RequestArgs: Encodable { } typealias Args = RequestArgs - typealias Response = String + typealias Response = BalancesResponse private var requestArgs: RequestArgs + private let success: ([Balance]) -> Void + private let failure: () -> Void - init() { - requestArgs = RequestArgs() + init(onSuccess: @escaping ([Balance]) -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs() + self.success = onSuccess + self.failure = onFailure } func operation() -> String { - return "getTransactions" + return "getBalances" } func args() -> Args { @@ -104,41 +124,312 @@ fileprivate struct WalletBackendGetTransactionsRequest: WalletBackendRequest { } func success(result: Response) { - + self.success(result.balances) } func error(_ err: WalletBackendResponseError) { - + self.failure() } } -struct WalletBackendGetBalancesRequest: WalletBackendRequest { - struct Balance: Decodable { - var available: Amount - var pendingIncoming: Amount - var pendingOutgoing: Amount - var requiresUserInput: Bool +/** + A timestamp, measured in milliseconds elapsed from an epoch. + */ +enum TimestampError: Error { + case invalidString +} + +enum Timestamp: Codable, Equatable { + case millisecondsSinceEpoch(Int) + case never + + enum CodingKeys: String, CodingKey { + case t_ms = "t_ms" } - struct BalancesResponse: Decodable { - var balances: [Balance] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + self = Timestamp.millisecondsSinceEpoch(try container.decode(Int.self, forKey: .t_ms)) + } catch { + let stringValue = try container.decode(String.self, forKey: .t_ms) + if stringValue == "never" { + self = Timestamp.never + } else { + throw TimestampError.invalidString + } + } } + + func encode(to encoder: Encoder) throws { + var value = encoder.singleValueContainer() + switch self { + case .millisecondsSinceEpoch(let t_ms): + try value.encode(t_ms) + case .never: + try value.encode("never") + } + } +} + +/** + A billing or mailing location. + */ +struct Location: Codable { + var country: String? + var country_subdivision: String? + var district: String? + var town: String? + var town_location: String? + var post_code: String? + var street: String? + var building_name: String? + var building_number: String? + var address_lines: [String]? +} + +/** + Information identifying a merchant. + */ +struct Merchant: Codable { + var name: String + var address: Location? + var jurisdiction: Location? +} + +/** + A tax made on a payment. + */ +struct Tax: Codable { + var name: String + var tax: Amount +} + +/** + A product being purchased from a merchant. + */ +struct Product: Codable { + var product_id: String? + var description: String + // description_i18n? + var quantity: Int + var unit: String + var price: Amount? + var image: String // URL to a product image + var taxes: [Tax]? + var delivery_date: Timestamp? +} + +/** + Brief information about an order. + */ +struct OrderShortInfo: Codable { + var orderId: String + var merchant: Merchant + var summary: String + // summary_i18n? + var products: [Product] + var fulfillmentUrl: String? + var fulfillmentMessage: String? + // fulfillmentMessage_i18n? +} + +enum TransactionTypeError: Error { + case unknownTypeError +} + +/** + Different types of transactions. + */ +enum TransactionType: Codable { + case withdrawal + case payment + case refund + case tip + case refresh + + init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer() + let str = try value.decode(String.self) + let codingNames = [ + "TransactionWithdrawal" : TransactionType.withdrawal, + "TransactionPayment" : TransactionType.payment, + "TransactionRefund" : TransactionType.refund, + "TransactionTip" : TransactionType.tip, + "TransactionRefresh" : TransactionType.refresh + ] + if let type = codingNames[str] { + self = type + } else { + throw TransactionTypeError.unknownTypeError + } + } + + func encode(to encoder: Encoder) throws { + var value = encoder.singleValueContainer() + switch self { + case .withdrawal: + try value.encode("TransactionWithdrawal") + case .payment: + try value.encode("TransactionPayment") + case .refund: + try value.encode("TransactionRefund") + case .tip: + try value.encode("TransactionTip") + case .refresh: + try value.encode("TransactionRefresh") + } + } +} + +/** + An error associated with a transaction. + */ +struct TransactionError: Codable { + var ec: Int + var hint: String? + //var details: Any? +} + +/** + A wallet transaction. + */ +struct Transaction: Codable { + var transactionId: String + var type: TransactionType + var timestamp: Timestamp + var pending: Bool + var error: TransactionError? + var amountRaw: Amount + var amountEffective: Amount +} + +/** + A request to get the transactions in the wallet's history. + */ +class WalletBackendGetTransactionsRequest: WalletBackendRequest { struct RequestArgs: Encodable { + var currency: String? + var search: String? + } + struct TransactionsResponse: Decodable { } typealias Args = RequestArgs - typealias Response = BalancesResponse + typealias Response = TransactionsResponse private var requestArgs: RequestArgs - private let success: ([Balance]) -> Void + private let success: () -> Void private let failure: () -> Void - init(onSuccess: @escaping ([Balance]) -> Void, onFailure: @escaping () -> Void) { - self.requestArgs = RequestArgs() + init(onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(currency: nil, search: nil) + self.success = onSuccess + self.failure = onFailure + } + + init(filterCurrency: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(currency: filterCurrency, search: nil) + self.success = onSuccess + self.failure = onFailure + } + + init(searchString: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(currency: nil, search: searchString) + self.success = onSuccess + self.failure = onFailure + } + + init(filterCurrency: String, searchString: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(currency: filterCurrency, search: searchString) self.success = onSuccess self.failure = onFailure } func operation() -> String { - return "getBalances" + return "getTransactions" + } + + func args() -> Args { + return requestArgs + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to delete a wallet transaction by ID. + */ +class WalletBackendDeleteTransactionRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var transactionId: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private var requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(transactionID: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(transactionId: transactionID) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "deleteTransaction" + } + + func args() -> Args { + return requestArgs + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to process a refund. + */ +class WalletBackendApplyRefundRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var talerRefundUri: String + } + struct RequestResponse: Decodable { + var contractTermsHash: String + var amountEffectivePaid: Amount + var amountRefundGranted: Amount + var amountRefundGone: Amount + var pendingAtExchange: Bool + var info: OrderShortInfo + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: (RequestResponse) -> Void + private let failure: () -> Void + + init(refundURI: String, onSuccess: @escaping (RequestResponse) -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(talerRefundUri: refundURI) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "applyRefund" } func args() -> Args { @@ -146,17 +437,667 @@ struct WalletBackendGetBalancesRequest: WalletBackendRequest { } func success(result: Response) { - print("balances???") + self.success(result) + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to list exchanges. + */ +class WalletBackendListExchanges: WalletBackendRequest { + struct RequestArgs: Encodable { + + } + struct ExchangeListItem: Decodable { + var exchangeBaseUrl: String + var currency: String + var paytoUris: [String] + } + struct RequestResponse: Decodable { + var exchanges: [ExchangeListItem] + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let success: ([ExchangeListItem]) -> Void + private let failure: () -> Void + + init(onSuccess: @escaping ([ExchangeListItem]) -> Void, onFailure: @escaping () -> Void) { + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "listExchanges" + } + + func args() -> RequestArgs { + return RequestArgs() + } + + func success(result: RequestResponse) { + self.success(result.exchanges) } func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to add an exchange. + */ +class WalletBackendAddRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var exchangeBaseUrl: String + } + struct RequestResponse: Decodable { } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "addRequest" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } } -struct WalletBackendWithdrawTestBalance: WalletBackendRequest { +/** + A request to force update an exchange. + */ +class WalletBackendForceUpdateRequest: WalletBackendRequest { struct RequestArgs: Encodable { - var amount: String + var exchangeBaseUrl: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "addRequest" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to query an exchange's terms of service. + */ +class WalletBackendGetExchangeTermsOfService: WalletBackendRequest { + struct RequestArgs: Encodable { + var exchangeBaseUrl: String + } + struct RequestResponse: Decodable { + var tos: String + var currentEtag: String + var acceptedEtag: String + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "getExchangeTos" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to mark an exchange's terms of service as accepted. + */ +class WalletBackendSetExchangeTermsOfServiceAccepted: WalletBackendRequest { + struct RequestArgs: Encodable { + var exchangeBaseUrl: String + var acceptedEtag: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, acceptedEtag: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL, acceptedEtag: acceptedEtag) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "setExchangeTosAccepted" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +struct ExchangeListItem: Codable { + var exchangeBaseUrl: String + var currency: String + var paytoUris: [String] +} + +/** + A request to get an exchange's withdrawal details. + */ +class WalletBackendGetWithdrawalDetailsForURIRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var talerWithdrawUri: String + } + struct RequestResponse: Decodable { + var amount: Amount + var defaultExchangeBaseUrl: String? + var possibleExchanges: [ExchangeListItem] + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(talerWithdrawUri: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(talerWithdrawUri: talerWithdrawUri) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "getWithdrawalDetailsForUri" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to get an exchange's withdrawal details. + */ +class WalletBackendGetWithdrawalDetailsForAmountRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var exchangeBaseUrl: String + var amount: Amount + } + struct RequestResponse: Decodable { + var tosAccepted: Bool + var amountRaw: Amount + var amountEffective: Amount + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, amount: Amount, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL, amount: amount) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "getWithdrawalDetailsForAmount" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to accept a bank-integrated withdrawl. + */ +class WalletBackendAcceptBankIntegratedWithdrawalRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var talerWithdrawUri: String + var exchangeBaseUrl: String + } + struct RequestResponse: Decodable { + var bankConfirmationUrl: String? + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(talerWithdrawURI: String, exchangeBaseURL: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(talerWithdrawUri: talerWithdrawURI, exchangeBaseUrl: exchangeBaseURL) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "acceptWithdrawal" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to accept a manual withdrawl. + */ +class WalletBackendAcceptManualWithdrawalRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var exchangeBaseUrl: String + var amount: Amount + } + struct RequestResponse: Decodable { + var exchangePaytoUris: [String] + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(exchangeBaseURL: String, amount: Amount, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(exchangeBaseUrl: exchangeBaseURL, amount: amount) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "acceptManualWithdrawal" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to deposit funds. + */ +class WalletBackendCreateDepositGroupRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var depositPayToUri: String + var amount: Amount + } + struct RequestResponse: Decodable { + var depositGroupId: String + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(depositPayToUri: String, amount: Amount, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(depositPayToUri: depositPayToUri, amount: amount) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "createDepositGroup" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to get information about a payment request. + */ +class WalletBackendPreparePayRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var talerPayUri: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(talerPayUri: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(talerPayUri: talerPayUri) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "preparePay" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to confirm a payment. + */ +class WalletBackendConfirmPayRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var proposalId: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(proposalId: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(proposalId: proposalId) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "abortFailedPayWithRefund" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to prepare a tip. + */ +class WalletBackendPrepareTipRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var talerTipUri: String + } + struct RequestResponse: Decodable { + var walletTipId: String + var accepted: Bool + var tipAmountRaw: Amount + var tipAmountEffective: Amount + var exchangeBaseUrl: String + var expirationTimestamp: Timestamp + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(talerTipURI: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(talerTipUri: talerTipURI) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "prepareTip" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to accept a tip. + */ +class WalletBackendAcceptTipRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var walletTipId: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(walletTipId: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(walletTipId: walletTipId) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "acceptTip" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to abort a failed payment. + */ +class WalletBackendAbortFailedPaymentRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var proposalId: String + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(proposalId: String, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(proposalId: proposalId) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "confirmPay" + } + + func args() -> RequestArgs { + return requestArgs + } + + func success(result: RequestResponse) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to withdraw a balance from the TESTKUDOS environment. + */ +class WalletBackendWithdrawTestkudosRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let success: () -> Void + private let failure: () -> Void + + init(onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "withdrawTestkudos" + } + + func args() -> Args { + return RequestArgs() + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to add a test balance to the wallet. + */ +class WalletBackendWithdrawTestBalance: WalletBackendRequest { + struct RequestArgs: Encodable { + var amount: Amount var bankBaseUrl: String var exchangeBaseUrl: String } @@ -164,7 +1105,7 @@ struct WalletBackendWithdrawTestBalance: WalletBackendRequest { typealias Response = String private var requestArgs: RequestArgs - init(amount: String, bankBaseUrl: String, exchangeBaseUrl: String) { + init(amount: Amount, bankBaseUrl: String, exchangeBaseUrl: String) { requestArgs = RequestArgs(amount: amount, bankBaseUrl: bankBaseUrl, exchangeBaseUrl: exchangeBaseUrl) } @@ -185,6 +1126,183 @@ struct WalletBackendWithdrawTestBalance: WalletBackendRequest { } } +struct IntegrationTestArgs: Codable { + var exchangeBaseUrl: String + var bankBaseUrl: String + var merchantBaseUrl: String + var merchantApiKey: String + var amountToWithdraw: String + var amountToSpend: String +} + +/** + A request to run a basic integration test. + */ +class WalletBackendRunIntegrationTestRequest: WalletBackendRequest { + struct RequestResponse: Decodable { + + } + typealias Args = IntegrationTestArgs + typealias Response = RequestResponse + private let requestArgs: IntegrationTestArgs + private let success: () -> Void + private let failure: () -> Void + + init(args: IntegrationTestArgs, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = args + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "runIntegrationTest" + } + + func args() -> Args { + return requestArgs + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +struct TestPayArgs: Codable { + var merchantBaseUrl: String + var merchantApiKey: String + var amount: String + var summary: String +} + +/** + A request to make a test payment. + */ +class WalletBackendTestPayRequest: WalletBackendRequest { + struct RequestResponse: Decodable { + + } + typealias Args = TestPayArgs + typealias Response = RequestResponse + private let requestArgs: TestPayArgs + private let success: () -> Void + private let failure: () -> Void + + init(args: TestPayArgs, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = args + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "testPay" + } + + func args() -> Args { + return requestArgs + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +struct Coin: Codable { + var denom_pub: String + var denom_pub_hash: String + var denom_value: String + var coin_pub: String + var exchange_base_url: String + var remaining_value: String + var refresh_parent_coin_pub: String + var withdrawal_reserve_pub: String + var coin_suspended: Bool +} + +/** + A request to dump all coins to JSON. + */ +class WalletBackendDumpCoinsRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + + } + struct RequestResponse: Decodable { + var coins: [Coin] + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let success: () -> Void + private let failure: () -> Void + + init(onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "dumpCoins" + } + + func args() -> Args { + return RequestArgs() + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + +/** + A request to suspend or unsuspend a coin. + */ +class WalletBackendSuspendCoinRequest: WalletBackendRequest { + struct RequestArgs: Encodable { + var coinPub: String + var suspended: Bool + } + struct RequestResponse: Decodable { + + } + typealias Args = RequestArgs + typealias Response = RequestResponse + private let requestArgs: RequestArgs + private let success: () -> Void + private let failure: () -> Void + + init(coinPub: String, suspended: Bool, onSuccess: @escaping () -> Void, onFailure: @escaping () -> Void) { + self.requestArgs = RequestArgs(coinPub: coinPub, suspended: suspended) + self.success = onSuccess + self.failure = onFailure + } + + func operation() -> String { + return "setCoinSuspended" + } + + func args() -> Args { + return requestArgs + } + + func success(result: Response) { + self.success() + } + + func error(_ err: WalletBackendResponseError) { + self.failure() + } +} + enum WalletBackendError: Error { case serializationError case deserializationError @@ -234,7 +1352,7 @@ 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: { ([WalletBackendGetBalancesRequest.Balance]) -> Void in + try! sendRequest(request: WalletBackendGetBalancesRequest(onSuccess: { ([Balance]) -> Void in }, onFailure: { () -> Void in diff --git a/TalerTests/TimestampTests.swift b/TalerTests/TimestampTests.swift new file mode 100644 index 0000000..d2b2433 --- /dev/null +++ b/TalerTests/TimestampTests.swift @@ -0,0 +1,43 @@ +/* + * 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 XCTest +@testable import Taler + +class TimestampTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testTimestamps() throws { + let json1 = "{ \"t_ms\" : 12309487 }".data(using: .utf8)! + let json2 = "{ \"t_ms\" : \"never\" }".data(using: .utf8)! + let json3 = "{ \"t_ms\" : \"sometime\" }".data(using: .utf8)! + + var t: Timestamp = try! JSONDecoder().decode(Timestamp.self, from: json1) + XCTAssertEqual(t, Timestamp.millisecondsSinceEpoch(12309487)) + + t = try! JSONDecoder().decode(Timestamp.self, from: json2) + XCTAssertEqual(t, Timestamp.never) + XCTAssertThrowsError(t = try JSONDecoder().decode(Timestamp.self, from: json3)) + } + +} |