taler-ios

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

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 }