commit 0d3486f224f600aac7942ef14999b2779deb4638
parent c72370e05871f9f95fb7272c40c0aae2c51f3eb2
Author: Jonathan Buchanan <jonathan.russ.buchanan@gmail.com>
Date: Tue, 23 Aug 2022 15:39:31 -0400
parse withdraw transaction details
Diffstat:
7 files changed, 313 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
@@ -11,4 +11,4 @@ Before building anything, you should initialize and update the submodules by run
$ ./bootstrap
-To build the app, open `Taler.xcodeproj` with Xcode. The minimum version of iOS supported is 13.0. This iOS version is compatible on all iPhone models at least as new as the iPhone 6S.
+To build the app, open `Taler.xcodeproj` with Xcode. The minimum version of iOS supported is 14.0. This iOS version is compatible on all iPhone models at least as new as the iPhone 6S.
diff --git a/Taler.xcodeproj/project.pbxproj b/Taler.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
AB1F87C82887C94700AB82A0 /* TalerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C72887C94700AB82A0 /* TalerApp.swift */; };
AB1F87CA2887D2F400AB82A0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C92887D2F400AB82A0 /* ContentView.swift */; };
+ AB32199128B18859008AAC75 /* TransactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB32199028B18859008AAC75 /* TransactionsModel.swift */; };
AB4C534A28AC21C9003004F7 /* BalancesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4C534928AC21C9003004F7 /* BalancesView.swift */; };
AB4C534C28AC25FC003004F7 /* BalancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4C534B28AC25FC003004F7 /* BalancesModel.swift */; };
AB69F9FA28AAED53005CCC2E /* WithdrawModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB69F9F928AAED53005CCC2E /* WithdrawModel.swift */; };
@@ -60,6 +61,7 @@
/* Begin PBXFileReference section */
AB1F87C72887C94700AB82A0 /* TalerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalerApp.swift; sourceTree = "<group>"; };
AB1F87C92887D2F400AB82A0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+ AB32199028B18859008AAC75 /* TransactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsModel.swift; sourceTree = "<group>"; };
AB4C534928AC21C9003004F7 /* BalancesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesView.swift; sourceTree = "<group>"; };
AB4C534B28AC25FC003004F7 /* BalancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesModel.swift; sourceTree = "<group>"; };
AB69F9F928AAED53005CCC2E /* WithdrawModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawModel.swift; sourceTree = "<group>"; };
@@ -129,6 +131,7 @@
isa = PBXGroup;
children = (
ABB33066289C658900668B42 /* BackendManager.swift */,
+ AB32199028B18859008AAC75 /* TransactionsModel.swift */,
ABB33064289C5BBB00668B42 /* ExchangeManager.swift */,
ABC4AC3E28A473070047A56F /* PendingManager.swift */,
AB69F9F928AAED53005CCC2E /* WithdrawModel.swift */,
@@ -390,6 +393,7 @@
ABB33065289C5BBB00668B42 /* ExchangeManager.swift in Sources */,
AB4C534A28AC21C9003004F7 /* BalancesView.swift in Sources */,
D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */,
+ AB32199128B18859008AAC75 /* TransactionsModel.swift in Sources */,
ABB762AD2891059600E88634 /* SettingsView.swift in Sources */,
ABC4AC3B28A4619C0047A56F /* PendingView.swift in Sources */,
ABC4AC3F28A473070047A56F /* PendingManager.swift in Sources */,
diff --git a/Taler/Model/BalancesModel.swift b/Taler/Model/BalancesModel.swift
@@ -17,33 +17,32 @@
import Foundation
class BalancesModel: ObservableObject {
- enum State {
- case begin
- case loading
- case loaded([Balance])
- }
-
var backend: WalletBackend
- @Published var state: State
+
+ @Published var loading: Bool = false
+ @Published var balances: [Balance]?
init(backend: WalletBackend) {
self.backend = backend
- self.state = .begin
}
func getBalances() {
- self.state = .loading
+ self.loading = true
let req = WalletBackendGetBalancesRequest()
backend.sendFormattedRequest(request: req) { response, err in
// TODO: Use Combine instead
DispatchQueue.main.async {
+ self.loading = false
if let res = response {
- self.state = .loaded(res.balances)
+ self.balances = res.balances
} else {
// TODO: Handle error.
- self.state = .begin
}
}
}
}
+
+ func getTransactionsModel() -> TransactionsModel {
+ return TransactionsModel(backend: self.backend, currency: nil)
+ }
}
diff --git a/Taler/Model/TransactionsModel.swift b/Taler/Model/TransactionsModel.swift
@@ -0,0 +1,48 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 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
+
+class TransactionsModel: ObservableObject {
+ var backend: WalletBackend
+ var currency: String?
+
+ @Published var loading: Bool = false
+ @Published var transactions: [Transaction]?
+
+ init(backend: WalletBackend, currency: String?) {
+ self.backend = backend
+ self.currency = currency
+ }
+
+ func loadTransactions(searchString: String? = nil) {
+ self.loading = true
+ let req = WalletBackendGetTransactionsRequest(currency: self.currency,
+ search: searchString)
+ backend.sendFormattedRequest(request: req) { response, err in
+ // TODO: Use Combine instead
+ DispatchQueue.main.async {
+ self.loading = false
+ if let res = response {
+ print("x")
+ self.transactions = res.transactions
+ } else {
+ // TODO: Handle error.
+ }
+ }
+ }
+ }
+}
diff --git a/Taler/Views/BalancesView.swift b/Taler/Views/BalancesView.swift
@@ -16,6 +16,47 @@
import SwiftUI
+struct TransactionsView: View {
+ @ObservedObject var model: TransactionsModel
+
+ var body: some View {
+ VStack {
+ if model.transactions == nil {
+ ProgressView()
+ .onAppear {
+ model.loadTransactions()
+ }
+ } else if model.loading {
+ ProgressView()
+ } else {
+ List(model.transactions!, id: \.self) { tx in
+ VStack {
+ Text("Transaction: \(tx.transactionId)")
+ }
+ }
+ Text("Loaded")
+ /*VStack {
+ Text("Balances")
+ NavigationLink {
+ TransactionsView(model: self.balancesModel.getTransactionsModel())
+ } label: {
+ Text("Transactions")
+ }
+
+ }
+ .padding(16)
+ .navigationTitle("Balances")
+ .navigationBarItems(
+ leading: Button(action: self.showSidebar, label: {
+ Image(systemName: "line.3.horizontal")
+ })
+ )*/
+ }
+ }
+ .navigationTitle("Transactions")
+ }
+}
+
struct BalancesView: View {
@ObservedObject var balancesModel: BalancesModel
@EnvironmentObject var backend: BackendManager
@@ -23,19 +64,18 @@ struct BalancesView: View {
var body: some View {
NavigationView {
- switch balancesModel.state {
- case .begin:
- EmptyView()
- .onAppear(perform: {
- balancesModel.getBalances()
- })
+ if balancesModel.balances == nil {
+ ProgressView()
.navigationTitle("Balances")
.navigationBarItems(
leading: Button(action: self.showSidebar, label: {
Image(systemName: "line.3.horizontal")
})
)
- case .loading:
+ .onAppear {
+ balancesModel.getBalances()
+ }
+ } else if balancesModel.loading {
ProgressView()
.navigationTitle("Balances")
.navigationBarItems(
@@ -43,9 +83,15 @@ struct BalancesView: View {
Image(systemName: "line.3.horizontal")
})
)
- case .loaded(let balances):
+ } else {
VStack {
Text("Balances")
+ NavigationLink {
+ TransactionsView(model: self.balancesModel.getTransactionsModel())
+ } label: {
+ Text("Transactions")
+ }
+
}
.padding(16)
.navigationTitle("Balances")
@@ -57,8 +103,4 @@ struct BalancesView: View {
}
}
}
-
- /*init(showSidebar: @escaping () -> Void) {
- self.showSidebar = showSidebar
- }*/
}
diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift
@@ -202,22 +202,203 @@ enum TransactionType: Codable {
}
}
-/// An error associated with a transaction.
-struct TransactionError: Codable {
- var ec: Int
- var hint: String?
- //var details: Any?
+enum TransactionDecodingError: Error {
+ case invalidStringValue
+}
+
+/// Details for a manual withdrawal.
+struct ManualWithdrawalDetails: Codable {
+ /// The payto URIs that the exchange supports.
+ var exchangePaytoUris: [String]
+
+ /// The public key of the newly created reserve.
+ var reservePub: String
+}
+
+/// Details for a bank-integrated withdrawal.
+struct BankIntegratedWithdrawalDetails: Codable {
+ /// Whether the bank has confirmed the withdrawal.
+ var confirmed: Bool
+
+ /// URL for user-initiated confirmation
+ var bankConfirmationUrl: String?
+}
+
+/// A withdrawal transaction.
+struct TransactionWithdrawal: Decodable {
+ enum WithdrawalDetails {
+ case manual(ManualWithdrawalDetails)
+ case bankIntegrated(BankIntegratedWithdrawalDetails)
+ }
+
+ /// The exchange that was withdrawn from.
+ var exchangeBaseUrl: String
+
+ /// The amount of the withdrawal, including fees.
+ var amountRaw: Amount
+
+ /// The amount that will be added to the withdrawer's account.
+ var amountEffective: Amount
+
+ /// The details of the withdrawal.
+ var withdrawalDetails: WithdrawalDetails
+
+ init(from decoder: Decoder) throws {
+ enum CodingKeys: String, CodingKey {
+ case exchangeBaseUrl
+ case amountRaw
+ case amountEffective
+ case withdrawalDetails
+ case type
+ case exchangePaytoUris
+ case reservePub
+ case confirmed
+ case bankConfirmationUrl
+ }
+
+ let value = try decoder.container(keyedBy: CodingKeys.self)
+ self.exchangeBaseUrl = try value.decode(String.self, forKey: .exchangeBaseUrl)
+ self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
+ self.amountEffective = try value.decode(Amount.self, forKey: .amountEffective)
+
+ let detail = try value.nestedContainer(keyedBy: CodingKeys.self, forKey: .withdrawalDetails)
+ let detailType = try detail.decode(String.self, forKey: .type)
+ if detailType == "manual-transfer" {
+ let paytoUris = try detail.decode([String].self, forKey: .exchangePaytoUris)
+ let reservePub = try detail.decode(String.self, forKey: .reservePub)
+ let manual = ManualWithdrawalDetails(exchangePaytoUris: paytoUris, reservePub: reservePub)
+ self.withdrawalDetails = .manual(manual)
+ } else if detailType == "taler-bank-integration-api" {
+ let confirmed = try detail.decode(Bool.self, forKey: .confirmed)
+ var bankConfirmationUrl: String? = nil
+ if detail.contains(.bankConfirmationUrl) {
+ bankConfirmationUrl = try detail.decode(String.self, forKey: .bankConfirmationUrl)
+ }
+ let bankDetails = BankIntegratedWithdrawalDetails(confirmed: confirmed, bankConfirmationUrl: bankConfirmationUrl)
+ self.withdrawalDetails = .bankIntegrated(bankDetails)
+ } else {
+ throw TransactionDecodingError.invalidStringValue
+ }
+ }
+}
+
+/// A payment transaction.
+struct TransactionPayment: Codable {
+ /// Additional information about the payment.
+ // TODO
+
+ /// An identifier for the payment.
+ var proposalId: String
+
+ /// The current status of the payment.
+ // TODO
+
+ /// The amount that must be paid.
+ var amountRaw: Amount
+
+ /// The amount that was paid.
+ var amountEffective: Amount
+}
+
+/// A refund transaction.
+struct TransactionRefund: Codable {
+ /// Identifier for the refund.
+ var refundedTransactionId: String
+
+ /// Additional information about the refund
+ // TODO
+
+ /// The amount that couldn't be applied because refund permissions expired.
+ var amountInvalid: Amount
+
+ /// The amount refunded by the merchant.
+ var amountRaw: Amount
+
+ /// The amount paid to the wallet after fees.
+ var amountEffective: Amount
+}
+
+/// A tip transaction.
+struct TransactionTip: Codable {
+ /// The current status of the tip.
+ // TODO
+
+ /// The exchange that the tip will be withdrawn from
+ var exchangeBaseUrl: String
+
+ /// More information about the merchant sending the tip.
+ // TODO
+
+ /// The raw amount of the tip without fees.
+ var amountRaw: Amount
+
+ /// The amount added to the recipient's wallet.
+ var amountEffective: Amount
+}
+
+/// A refresh transaction.
+struct TransactionRefresh: Codable {
+ /// The exchange that the coins are refreshed with.
+ var exchangeBaseUrl: String
+
+ /// The raw amount to refresh.
+ var amountRaw: Amount
+
+ /// The amount to be paid as fees for the refresh.
+ var amountEffective: Amount
}
/// A wallet transaction.
-struct Transaction: Codable {
+struct Transaction: Decodable, Hashable {
+ enum TransactionDetail {
+ case withdrawal(TransactionWithdrawal)
+ }
+
var transactionId: String
- var type: TransactionType
var timestamp: Timestamp
var pending: Bool
- var error: TransactionError?
+ var error: AnyCodable?
var amountRaw: Amount
var amountEffective: Amount
+ var detail: TransactionDetail
+
+ init(from decoder: Decoder) throws {
+ enum CodingKeys: String, CodingKey {
+ case transactionId
+ case timestamp
+ case pending
+ case error
+ case amountRaw
+ case amountEffective
+ case type
+ }
+
+ let value = try decoder.container(keyedBy: CodingKeys.self)
+ self.transactionId = try value.decode(String.self, forKey: .transactionId)
+ self.timestamp = try value.decode(Timestamp.self, forKey: .timestamp)
+ self.pending = try value.decode(Bool.self, forKey: .pending)
+ if value.contains(.error) {
+ self.error = try value.decode(AnyCodable.self, forKey: .error)
+ }
+ self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
+ self.amountEffective = try value.decode(Amount.self, forKey: .amountEffective)
+
+ let type = try value.decode(String.self, forKey: .type)
+ if type == "withdrawal" {
+ let withdrawDetail = try TransactionWithdrawal.init(from: decoder)
+ self.detail = .withdrawal(withdrawDetail)
+ } else {
+ throw TransactionDecodingError.invalidStringValue
+ }
+ }
+
+ static func == (lhs: Transaction, rhs: Transaction) -> Bool {
+ return lhs.transactionId == rhs.transactionId
+ }
+
+ func hash(into hasher: inout Hasher) {
+ transactionId.hash(into: &hasher)
+ }
}
/// A request to get the transactions in the wallet's history.
@@ -231,7 +412,7 @@ struct WalletBackendGetTransactionsRequest: WalletBackendFormattedRequest {
}
struct Response: Decodable {
-
+ var transactions: [Transaction]
}
func operation() -> String {
@@ -901,6 +1082,7 @@ class WalletBackend: IonoMessageHandler {
}
func handleMessage(message: String) {
+ print(message)
do {
guard let messageData = message.data(using: .utf8) else { throw WalletBackendError.deserializationError }
let data = try JSONSerialization.jsonObject(with: messageData, options: .allowFragments) as? [String : Any]
diff --git a/taler-swift/Sources/taler-swift/Time.swift b/taler-swift/Sources/taler-swift/Time.swift
@@ -30,15 +30,15 @@ public enum Timestamp: Codable, Equatable {
case never
enum CodingKeys: String, CodingKey {
- case t_ms = "t_ms"
+ case t_s = "t_s"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
- self = Timestamp.milliseconds(try container.decode(UInt64.self, forKey: .t_ms))
+ self = Timestamp.milliseconds(try container.decode(UInt64.self, forKey: .t_s) * 1000)
} catch {
- let stringValue = try container.decode(String.self, forKey: .t_ms)
+ let stringValue = try container.decode(String.self, forKey: .t_s)
if stringValue == "never" {
self = Timestamp.never
} else {
@@ -55,9 +55,9 @@ public enum Timestamp: Codable, Equatable {
var value = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .milliseconds(let t_ms):
- try value.encode(t_ms, forKey: .t_ms)
+ try value.encode(t_ms / 1000, forKey: .t_s)
case .never:
- try value.encode("never", forKey: .t_ms)
+ try value.encode("never", forKey: .t_s)
}
}
}