Model+Transactions.swift (8423B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import Foundation 9 import taler_swift 10 import SymLog 11 import os.log 12 13 // MARK: - 14 extension WalletModel { 15 static func completedTransactions(_ transactions: [TalerTransaction]) -> [TalerTransaction] { 16 transactions.filter { transaction in 17 !transaction.isPending 18 } 19 } 20 static func pendingTransactions(_ transactions: [TalerTransaction]) -> [TalerTransaction] { 21 transactions.filter { transaction in 22 transaction.isPending 23 } 24 } 25 } 26 // MARK: - 27 enum TransactionStateFilter: String, Codable { 28 case done 29 case final // done, aborted, expired, ... 30 case nonfinal // pending, dialog, suspended, aborting 31 case nonfinalApproved = "nonfinal-approved" 32 case nonfinalDialog = "nonfinal-dialog" 33 } 34 struct TransactionOffset: Codable { 35 var txID: String 36 var txTime: Timestamp 37 } 38 /// A request to get the transactions in the wallet's history. 39 fileprivate struct GetTransactionsV2: WalletBackendFormattedRequest { 40 var operation: String { "getTransactionsV2" } 41 func args() -> Args { Args(scopeInfo: scope, 42 filterByState: filterByState, 43 includeRefreshes: includeRefreshes, 44 limit: -limit, // descending 45 offsetTransactionId: offset?.txID, 46 offsetTimestamp: offset?.txTime) } 47 48 var scope: ScopeInfo? 49 var filterByState: TransactionStateFilter? 50 var limit: Int 51 var offset: TransactionOffset? 52 var includeRefreshes: Bool? 53 struct Args: Encodable { 54 var scopeInfo: ScopeInfo? 55 var filterByState: TransactionStateFilter? 56 var includeRefreshes: Bool? 57 var limit: Int 58 var offsetTransactionId: String? 59 var offsetTimestamp: Timestamp? 60 } 61 62 struct Response: Decodable { // list of transactions 63 var transactions: [TalerTransaction] 64 65 enum CodingKeys: String, CodingKey { case transactions } 66 67 /// Decode the array element by element so that ONE transaction wallet-core sends 68 /// in a shape this app does not understand costs us that transaction, not the 69 /// user's entire history. Decoding `[TalerTransaction]` directly throws on the 70 /// first bad element and every caller's `try?` then renders an empty list. 71 init(from decoder: Decoder) throws { 72 let container = try decoder.container(keyedBy: CodingKeys.self) 73 let wrapped = try container.decode([SkippableTransaction].self, forKey: .transactions) 74 self.transactions = wrapped.compactMap { $0.transaction } 75 } 76 } 77 } 78 79 /// Wrapper whose own decoding never fails, so a failed element consumes its array slot 80 /// instead of aborting the array. See GetTransactionsV2.Response. 81 fileprivate struct SkippableTransaction: Decodable { 82 let transaction: TalerTransaction? 83 84 init(from decoder: Decoder) throws { 85 do { 86 transaction = try TalerTransaction(from: decoder) 87 } catch { 88 transaction = nil 89 let details = String(describing: error) 90 Logger(subsystem: "net.taler.gnu", category: "Transactions") 91 .error("dropped an undecodable transaction: \(details, privacy: .public)") 92 } 93 } 94 } 95 /// A request to abort a wallet transaction by ID. 96 struct AbortTransaction: WalletBackendFormattedRequest { 97 struct Response: Decodable {} // no result - getting no error back means success 98 var operation: String { "abortTransaction" } 99 func args() -> Args { Args(transactionId: transactionId) } 100 101 var transactionId: String 102 struct Args: Encodable { 103 var transactionId: String 104 } 105 } 106 /// A request to delete a wallet transaction by ID. 107 struct DeleteTransaction: WalletBackendFormattedRequest { 108 struct Response: Decodable {} // no result - getting no error back means success 109 var operation: String { "deleteTransaction" } 110 func args() -> Args { Args(transactionId: transactionId) } 111 112 var transactionId: String 113 struct Args: Encodable { 114 var transactionId: String 115 } 116 } 117 /// A request to delete a wallet transaction by ID. 118 struct FailTransaction: WalletBackendFormattedRequest { 119 struct Response: Decodable {} // no result - getting no error back means success 120 var operation: String { "failTransaction" } 121 func args() -> Args { Args(transactionId: transactionId) } 122 123 var transactionId: String 124 struct Args: Encodable { 125 var transactionId: String 126 } 127 } 128 /// A request to suspend a wallet transaction by ID. 129 struct SuspendTransaction: WalletBackendFormattedRequest { 130 struct Response: Decodable {} // no result - getting no error back means success 131 var operation: String { "suspendTransaction" } 132 func args() -> Args { Args(transactionId: transactionId) } 133 134 var transactionId: String 135 struct Args: Encodable { 136 var transactionId: String 137 } 138 } 139 /// A request to suspend a wallet transaction by ID. 140 struct ResumeTransaction: WalletBackendFormattedRequest { 141 struct Response: Decodable {} // no result - getting no error back means success 142 var operation: String { "resumeTransaction" } 143 func args() -> Args { Args(transactionId: transactionId) } 144 145 var transactionId: String 146 struct Args: Encodable { 147 var transactionId: String 148 } 149 } 150 // MARK: - 151 extension WalletModel { 152 /// ask wallet-core for its list of transactions filtered by state 153 nonisolated func getTransactionsV2(_ stack: CallStack, 154 scope: ScopeInfo, 155 filterByState: TransactionStateFilter, 156 limit: Int = 1_000_000, 157 offset: TransactionOffset? = nil, 158 includeRefreshes: Bool = false, 159 viewHandles: Bool = false) 160 async throws -> [TalerTransaction] { 161 let request = GetTransactionsV2(scope: scope, 162 filterByState: filterByState, 163 limit: limit, 164 offset: offset, 165 includeRefreshes: includeRefreshes) 166 let response = try await sendRequest(request, viewHandles: viewHandles) 167 return response.transactions 168 } 169 // MARK: - 170 /// abort the specified transaction from Wallet-Core. No networking involved 171 nonisolated func abortTransaction(transactionId: String, viewHandles: Bool = false) async throws { 172 let request = AbortTransaction(transactionId: transactionId) 173 logger.notice("abortTransaction: \(transactionId, privacy: .private(mask: .hash))") 174 let _ = try await sendRequest(request, viewHandles: viewHandles) 175 } 176 177 /// delete the specified transaction from Wallet-Core. No networking involved 178 nonisolated func deleteTransaction(transactionId: String, viewHandles: Bool = false) async throws { 179 let request = DeleteTransaction(transactionId: transactionId) 180 logger.notice("deleteTransaction: \(transactionId, privacy: .private(mask: .hash))") 181 let _ = try await sendRequest(request, viewHandles: viewHandles) 182 } 183 184 nonisolated func failTransaction(transactionId: String, viewHandles: Bool = false) async throws { 185 let request = FailTransaction(transactionId: transactionId) 186 logger.notice("failTransaction: \(transactionId, privacy: .private(mask: .hash))") 187 let _ = try await sendRequest(request, viewHandles: viewHandles) 188 } 189 190 nonisolated func suspendTransaction(transactionId: String, viewHandles: Bool = false) async throws { 191 let request = SuspendTransaction(transactionId: transactionId) 192 logger.notice("suspendTransaction: \(transactionId, privacy: .private(mask: .hash))") 193 let _ = try await sendRequest(request, viewHandles: viewHandles) 194 } 195 196 nonisolated func resumeTransaction(transactionId: String, viewHandles: Bool = false) async throws { 197 let request = ResumeTransaction(transactionId: transactionId) 198 logger.notice("resumeTransaction: \(transactionId, privacy: .private(mask: .hash))") 199 let _ = try await sendRequest(request, viewHandles: viewHandles) 200 } 201 }