taler-ios

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

WalletCore.swift (35031B)


      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  * @author Iván Ávalos
      8  */
      9 import SwiftUI              // FOUNDATION has no AppStorage
     10 import AnyCodable
     11 import SymLog
     12 import os
     13 import LocalConsole
     14 
     15 /// Delegate for the wallet backend.
     16 protocol WalletBackendDelegate {
     17     /// Called when the backend interface receives a message it does not know how to handle.
     18     func walletBackendReceivedUnknownMessage(_ walletCore: WalletCore, message: String)
     19 }
     20 
     21 // MARK: -
     22 /// An interface to the wallet backend.
     23 class WalletCore: QuickjsMessageHandler {
     24     public static let shared = try! WalletCore()          // will (and should) crash on failure
     25     private let symLog = SymLogC()
     26 
     27     private var queue: DispatchQueue
     28     private var semaphore: DispatchSemaphore
     29 
     30     private let quickjs: Quickjs
     31     private var requestsMade: UInt          // counter for array of completion closures
     32     private var completions: [UInt : (Date, (UInt, Date, String?, Data?, TalerErrorDetail?) -> Void)] = [:]
     33     var delegate: WalletBackendDelegate?
     34 
     35     var versionInfo: VersionInfo?           // shown in SettingsView
     36     var nativeDB: Bool
     37     var isObserving: Int
     38     var isLogging: Bool
     39     var logTransactions: Bool
     40     let logger = Logger(subsystem: "net.taler.gnu", category: "WalletCore")
     41 
     42     private var expired: [String] = []      // save txID of expired items to not beep twice
     43 
     44     private struct FullRequest: Encodable {
     45         let operation: String
     46         let id: UInt
     47         let args: AnyEncodable
     48     }
     49 
     50     private struct FullResponse: Decodable {
     51         let type: String
     52         let operation: String
     53         let id: UInt
     54         let result: AnyCodable
     55     }
     56     
     57     struct FullError: Decodable {
     58         let type: String
     59         let operation: String
     60         let id: UInt
     61         let error: TalerErrorDetail
     62     }
     63 
     64     var lastError: FullError?
     65 
     66     struct ResponseOrNotification: Decodable {
     67         let type: String
     68         let operation: String?
     69         let id: UInt?
     70         let result: AnyCodable?
     71         let error: TalerErrorDetail?
     72         let payload: AnyCodable?
     73     }
     74 
     75     /// the bare envelope, which still decodes when the full `ResponseOrNotification` doesn't
     76     private struct ResponseHeader: Decodable {
     77         let type: String
     78         let id: UInt?
     79     }
     80 
     81     struct Payload: Decodable {
     82         let type: String
     83         let id: String?
     84         let reservePub: String?
     85         let isInternal: Bool?
     86         let hintTransactionId: String?
     87         let event: [String: AnyCodable]?
     88 
     89         let operation: String?      // indexeddb-to-native-migration
     90         let phase: String?          // fixup | copy | verify | complete | failed
     91         let error: TalerErrorDetail?    // only when phase is "failed"
     92         let step: String?           // config | currencyInfo | contacts | mailboxMessages | mailboxConfigurations | contractTerms | tombstones | operationRetries | bankAccounts | globalCurrencyExchanges | globalCurrencyAuditors | exchangeBaseUrlFixups | exchangeBaseUrlMigrationLog | reserves | exchanges | exchangeDetails | exchangeSignKeys | denominationFamilies | denominations | withdrawalGroups | purchases | refreshGroups | coins | planchets | refreshSessions | coinHistory | coinAvailability | refundGroups | tokens | slates | depositGroups | recoupGroups | denomLossEvents | peerPushDebit | peerPushCredit | peerPullDebit | peerPullCredit | donationSummaries | donationPlanchets | donationReceipts | transactionsMeta | refundItems
     93         let completedSteps: Int?
     94         let totalSteps: Int?
     95         let processedRecords: Int?
     96         let totalRecords: Int?
     97         let completionPercent: Int?
     98     }
     99 
    100     deinit {
    101         logger.log("shutdown Quickjs")
    102     // TODO: send shutdown message to talerWalletInstance
    103 //        quickjs.waitStopped()
    104     }
    105 
    106     init() throws {
    107         nativeDB = false
    108         isObserving = 0
    109         isLogging = false
    110         logTransactions = false
    111 //        logger.trace("init Quickjs")
    112         requestsMade = 0
    113         queue = DispatchQueue(label: "net.taler.myQueue", attributes: .concurrent)
    114         semaphore = DispatchSemaphore(value: 1)
    115         quickjs = Quickjs()
    116         quickjs.messageHandler = self
    117         logger.log("Quickjs running")
    118     }
    119 }
    120 // MARK: -  completionHandler functions
    121 extension WalletCore {
    122     /// `requestsMade` and `completions` are touched both from the request queue and from
    123     /// wallet-core's message handler, thus every access must be guarded by the semaphore.
    124     private func reserveRequestId() -> UInt {
    125         semaphore.wait()
    126         defer { semaphore.signal() }
    127         let requestId = requestsMade
    128         requestsMade += 1
    129         return requestId
    130     }
    131 
    132     private func setCompletion(_ requestId: UInt, _ sendTime: Date,
    133                                _ completion: @escaping (UInt, Date, String?, Data?, TalerErrorDetail?) -> Void) {
    134         semaphore.wait()
    135         defer { semaphore.signal() }
    136         completions[requestId] = (sendTime, completion)
    137     }
    138 
    139     /// Take the completion out of the list, so that it can never be called twice.
    140     /// Whoever gets it must call it - on every path, including all error paths.
    141     private func takeCompletion(_ requestId: UInt)
    142                              -> (Date, (UInt, Date, String?, Data?, TalerErrorDetail?) -> Void)? {
    143         semaphore.wait()
    144         defer { semaphore.signal() }
    145         return completions.removeValue(forKey: requestId)
    146     }
    147 
    148     private func handleError(_ decoded: ResponseOrNotification, _ message: String?) throws {
    149         guard let requestId = decoded.id else {
    150             logger.error("didn't find requestId in error response")
    151             // TODO: show error alert
    152             throw WalletBackendError.deserializationError
    153         }
    154         guard let (timeSent, completion) = takeCompletion(requestId) else {
    155             logger.error("requestId \(requestId, privacy: .public) not in list")
    156             // TODO: show error alert
    157             throw WalletBackendError.deserializationError
    158         }
    159         if let walletError = decoded.error {            // wallet-core sent an error message
    160             do {
    161                 let jsonData = try JSONEncoder().encode(walletError)
    162                 let responseCode = walletError.code
    163                 logger.error("wallet-core sent back error \(walletError.code, privacy: .public), \(responseCode, privacy: .public) for request \(requestId, privacy: .public)")
    164                 symLog.log("id:\(requestId)  \(walletError)")
    165                 completion(requestId, timeSent, message, jsonData, walletError)
    166             } catch {        // JSON encoding of response.result failed / should never happen
    167                 symLog.log(decoded)
    168                 logger.error("cannot encode wallet-core Error")
    169                 completion(requestId, timeSent, message, nil, WalletCore.parseFailureError())
    170             }
    171         } else {             // JSON decoding of error message failed
    172             completion(requestId, timeSent, message, nil, WalletCore.parseFailureError())
    173         }
    174     }
    175 
    176     private func handleResponse(_ decoded: ResponseOrNotification, _ message: String) throws {
    177         guard let requestId = decoded.id else {
    178             logger.error("didn't find requestId in response")
    179             symLog.log(decoded)                 // TODO: .error
    180             throw WalletBackendError.deserializationError
    181         }
    182         guard let (timeSent, completion) = takeCompletion(requestId) else {
    183             logger.error("requestId \(requestId, privacy: .public) not in list")
    184             throw WalletBackendError.deserializationError
    185         }
    186         guard let result = decoded.result else {        // don't throw - we own the completion now
    187             logger.error("requestId \(requestId, privacy: .public) got no result")
    188             completion(requestId, timeSent, message, nil, WalletCore.parseResponseError())
    189             return
    190         }
    191         do {
    192             let jsonData = try JSONEncoder().encode(result)
    193             if let operation = decoded.operation {
    194                 if operation == "getTransactionsV2" {
    195                     if logTransactions {
    196                         symLog.log(message)
    197                     }
    198                 } else {
    199                     if #available(iOS 16.0, *) {
    200                         let regex = #/"data:image(.*?)"/#
    201                         let modString = message.replacing(regex, with: "\"XXX\"")
    202                         symLog.log(modString)
    203                     } else {
    204                         symLog.log(message)
    205                     }
    206 
    207                 }
    208             }
    209 //            logger.info(result)   TODO: log result
    210             completion(requestId, timeSent, message, jsonData, nil)
    211         } catch {        // JSON encoding of response.result failed / should never happen
    212             symLog.log(result)                 // TODO: .error
    213             completion(requestId, timeSent, message, nil, WalletCore.parseResponseError())
    214         }
    215     }
    216 
    217     @MainActor
    218     private func postNotificationM(_ aName: NSNotification.Name,
    219                            object anObject: Any? = nil,
    220                                   userInfo: [AnyHashable: Any]? = nil) async {
    221         NotificationCenter.default.post(name: aName, object: anObject, userInfo: userInfo)
    222     }
    223     private func postNotification(_ aName: NSNotification.Name,
    224                           object anObject: Any? = nil,
    225                                  userInfo: [AnyHashable: Any]? = nil) {
    226         Task { // runs on MainActor
    227             await postNotificationM(aName, object: anObject, userInfo: userInfo)
    228 //            logger.info("Notification sent: \(aName.rawValue, privacy: .public)")
    229         }
    230     }
    231 
    232     @MainActor
    233     private func handleRequestProgressError(_ jsonData: Data) throws {
    234         do {
    235             let decoded = try JSONDecoder().decode(RequestProgressError.self, from: jsonData)
    236             DispatchQueue.main.async {
    237                 Controller.shared.lastProgressError = decoded
    238                 self.postNotification(.RequestProgressError,
    239                                       userInfo: [NOTIFICATIONERROR: decoded])
    240             }
    241         }
    242     }
    243 
    244     @MainActor
    245     private func handleRequestProgressPhase(_ jsonData: Data) throws {
    246         do {
    247             let decoded = try JSONDecoder().decode(RequestProgressPhase.self, from: jsonData)
    248             DispatchQueue.main.async {
    249                 Controller.shared.lastProgressPhase = decoded
    250                 self.postNotification(.RequestProgressPhase,
    251                              userInfo: [NOTIFICATIONPHASE: decoded])
    252             }
    253         }
    254     }
    255 
    256     private func handlePendingProcessed(_ payload: Payload) throws {
    257         guard let id = payload.id else {
    258             throw WalletBackendError.deserializationError
    259         }
    260         let pendingOp = Notification.Name.PendingOperationProcessed.rawValue
    261         if id.hasPrefix("exchange-update:") {
    262             // Bla Bla Bla
    263         } else if id.hasPrefix("refresh:") {
    264             // Bla Bla Bla
    265         } else if id.hasPrefix("purchase:") {
    266             // TODO: handle purchase
    267 //            symLog.log("\(pendingOp): \(id)")
    268         } else if id.hasPrefix("withdraw:") {
    269             // TODO: handle withdraw
    270 //            symLog.log("\(pendingOp): \(id)")
    271         } else if id.hasPrefix("peer-pull-credit:") {
    272             // TODO: handle peer-pull-credit
    273 //            symLog.log("\(pendingOp): \(id)")
    274         } else if id.hasPrefix("peer-push-debit:") {
    275             // TODO: handle peer-push-debit
    276 //            symLog.log("\(pendingOp): \(id)")
    277         } else {
    278             // TODO: handle other pending-operation-processed
    279             logger.log("❗️ \(pendingOp, privacy: .public): \(id, privacy: .public)")        // this is a new pendingOp I haven't seen before
    280         }
    281     }
    282     @MainActor
    283     private func handleStateTransition(_ jsonData: Data) throws {
    284         do {
    285             let decoded = try JSONDecoder().decode(TransactionTransition.self, from: jsonData)
    286             if let errorInfo = decoded.errorInfo {
    287                 // reload pending transaction list to add error badge
    288                 postNotification(.TransactionError, userInfo: [NOTIFICATIONERROR: WalletBackendError.walletCoreError(errorInfo)])
    289             } else {
    290                 guard decoded.newTxState != decoded.oldTxState else {
    291                     logger.info("handleStateTransition: No State change: \(decoded.transactionId, privacy: .private(mask: .hash))")
    292                     return
    293                 }
    294             }
    295 
    296             let components = decoded.transactionId.components(separatedBy: ":")
    297             if components.count >= 3 {  // txn:$txtype:$uid
    298                 if let type = TransactionType(rawValue: components[1]) {
    299                     guard type != .refresh else { return }
    300                     let newMajor = decoded.newTxState.major
    301                     let newMinor = decoded.newTxState.minor
    302                     let oldMinor = decoded.oldTxState?.minor
    303                     switch newMajor {
    304                         case .done:
    305                             logger.info("handleStateTransition: Done: \(decoded.transactionId, privacy: .private(mask: .hash))")
    306                             if type.isWithdrawal {
    307                                 Controller.shared.playSound(2)  // play payment_received only for withdrawals
    308                             } else if !type.isIncoming {
    309                                 if !(oldMinor == .autoRefund || oldMinor == .acceptRefund) {
    310                                     Controller.shared.playSound(1)  // play payment_sent for all outgoing tx
    311                                 }
    312                             } else {    // incoming but not withdrawal
    313                                 logger.info("    incoming payment done - NO sound - \(type.rawValue)")
    314                             }
    315                             postNotification(.TransactionDone, userInfo: [TRANSACTIONTRANSITION: decoded])
    316                             return
    317                         case .aborting, .aborted:
    318                             logger.log("handleStateTransition: Aborting: \(decoded.transactionId, privacy: .private(mask: .hash))")
    319                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    320                         case .expired, .deleted:
    321                             logger.warning("handleStateTransition: Expired: \(decoded.transactionId, privacy: .private(mask: .hash))")
    322                             if let index = expired.firstIndex(of: components[2]) {
    323                                 expired.remove(at: index)                       // don't beep twice
    324                             } else {
    325                                 expired.append(components[2])
    326                                 Controller.shared.playSound(0)                  // beep at first sight
    327                             }
    328                             postNotification(.TransactionExpired, userInfo: [TRANSACTIONTRANSITION: decoded])
    329                         case .pending:
    330                             if let newMinor {
    331                                 if newMinor == .ready {
    332                                     logger.log("handleStateTransition: PendingReady: \(decoded.transactionId, privacy: .private(mask: .hash))")
    333                                     postNotification(.PendingReady, userInfo: [TRANSACTIONTRANSITION: decoded])
    334                                     return
    335                                 } else if newMinor == .exchangeWaitReserve      // user did confirm on bank website
    336                                        || newMinor == .withdraw {               // coin-withdrawal has started
    337 //                                    logger.log("DismissSheet: \(decoded.transactionId, privacy: .private(mask: .hash))")
    338                                     postNotification(.DismissSheet, userInfo: [TRANSACTIONTRANSITION: decoded])
    339                                     return
    340                                 } else if newMinor == .kyc {       // user did confirm on bank website, but KYC is needed
    341                                     logger.log("handleStateTransition: KYCrequired: \(decoded.transactionId, privacy: .private(mask: .hash))")
    342                                     postNotification(.KYCrequired, userInfo: [TRANSACTIONTRANSITION: decoded])
    343                                     return
    344                                 }
    345                                 logger.trace("handleStateTransition: Pending:\(newMinor.rawValue, privacy: .public) \(decoded.transactionId, privacy: .private(mask: .hash))")
    346                             } else {
    347                                 logger.trace("handleStateTransition: Pending: \(decoded.transactionId, privacy: .private(mask: .hash))")
    348                             }
    349                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    350                         default:
    351                             if let newMinor {
    352                                 logger.log("handleStateTransition: \(newMajor.rawValue, privacy: .public):\(newMinor.rawValue, privacy: .public) \(decoded.transactionId, privacy: .private(mask: .hash))")
    353                             } else {
    354                                 logger.warning("handleStateTransition: \(newMajor.rawValue, privacy: .public): \(decoded.transactionId, privacy: .private(mask: .hash))")
    355                             }
    356                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    357                     } // switch
    358                 } // type
    359             } // 3 components
    360             return
    361         } catch DecodingError.dataCorrupted(let context) {
    362             logger.error("handleStateTransition: \(context.debugDescription)")
    363         } catch DecodingError.keyNotFound(let key, let context) {
    364             logger.error("handleStateTransition: Key '\(key.stringValue)' not found:\(context.debugDescription)")
    365             logger.error("\(context.codingPath)")
    366         } catch DecodingError.valueNotFound(let value, let context) {
    367             logger.error("handleStateTransition: Value '\(value)' not found:\(context.debugDescription)")
    368             logger.error("\(context.codingPath)")
    369         } catch DecodingError.typeMismatch(let type, let context) {
    370             logger.error("handleStateTransition: Type '\(type)' mismatch:\(context.debugDescription)")
    371             logger.error("\(context.codingPath)")
    372         } catch let error {       // rethrows
    373             logger.error("handleStateTransition: \(error.localizedDescription)")
    374         }
    375         throw WalletBackendError.walletCoreError(nil)       // TODO: error?
    376     }
    377 
    378     @MainActor private func handleNotification(_ anyCodable: AnyCodable?, _ message: String) throws {
    379         guard let anyPayload = anyCodable else {
    380             throw WalletBackendError.deserializationError
    381         }
    382         do {
    383             let jsonData = try JSONEncoder().encode(anyPayload)
    384             let payload = try JSONDecoder().decode(Payload.self, from: jsonData)
    385 
    386             switch payload.type {
    387                 case Notification.Name.Idle.rawValue:
    388 //                    symLog.log(message)
    389                     break
    390                 case Notification.Name.DatabaseMaintenance.rawValue:
    391                     symLog.log(message)
    392                     postNotification(.DatabaseMaintenance, userInfo: [NOTIFICATIONPAYLOAD: payload])
    393                     break
    394                 case Notification.Name.ExchangeStateTransition.rawValue:
    395                     symLog.log(message)
    396                     break
    397                 case Notification.Name.RequestProgressError.rawValue:
    398                     symLog.log(message)
    399                     try handleRequestProgressError(jsonData)
    400                 case Notification.Name.RequestProgressPhase.rawValue:
    401                     symLog.log(message)
    402                     try handleRequestProgressPhase(jsonData)
    403                 case Notification.Name.TransactionStateTransition.rawValue:
    404                     symLog.log(message)
    405                     try handleStateTransition(jsonData)
    406                 case Notification.Name.PendingOperationProcessed.rawValue:
    407                     try handlePendingProcessed(payload)
    408                 case Notification.Name.BalanceChange.rawValue:
    409                     let now = Date()
    410                     symLog.log(message)
    411                     if !(payload.isInternal ?? false) {     // don't re-post internals
    412                         if let txID = payload.hintTransactionId {
    413                             if txID.contains("txn:refresh:") {
    414                                 break                       // don't re-post refresh
    415                             }
    416                         }
    417                         postNotification(.BalanceChange, userInfo: [NOTIFICATIONTIME: now])
    418                     }
    419                 case Notification.Name.BankAccountChange.rawValue:
    420                     symLog.log(message)
    421                     postNotification(.BankAccountChange)
    422                 case Notification.Name.ExchangeAdded.rawValue:
    423                     symLog.log(message)
    424                     postNotification(.ExchangeAdded)
    425                 case Notification.Name.ExchangeDeleted.rawValue:
    426                     symLog.log(message)
    427                     postNotification(.ExchangeDeleted)
    428                 case Notification.Name.ReserveNotYetFound.rawValue:
    429                     if let reservePub = payload.reservePub {
    430                         let userInfo = ["reservePub" : reservePub]
    431 //                        postNotification(.ReserveNotYetFound, userInfo: userInfo)   // TODO: remind User to confirm withdrawal
    432                     } // else { throw WalletBackendError.deserializationError }   shouldn't happen, but if it does just ignore it
    433 
    434                 case Notification.Name.ProposalAccepted.rawValue:               // "proposal-accepted":
    435                     symLog.log(message)
    436                     postNotification(.ProposalAccepted, userInfo: nil)
    437                 case Notification.Name.ProposalDownloaded.rawValue:             // "proposal-downloaded":
    438                     symLog.log(message)
    439                     postNotification(.ProposalDownloaded, userInfo: nil)
    440                 case Notification.Name.TaskObservabilityEvent.rawValue,
    441                      Notification.Name.RequestObservabilityEvent.rawValue:
    442                     if isObserving != 0 {
    443                         symLog.log(message)
    444                         let timestamp = TalerDater.dateString()
    445                         if let event = payload.event, let json = event.toJSON() {
    446                             let type = event["type"]?.value as? String
    447                             let eventID = event["id"]?.value as? String
    448                             if #available(iOS 16.0, *) {
    449                                 observe(json: json,
    450                                         type: type,
    451                                      eventID: eventID,
    452                                    timestamp: timestamp)
    453                             }
    454                         }
    455                     }
    456                     // TODO: remove these once wallet-core doesn't send them anymore
    457 //                case "refresh-started", "refresh-melted",
    458 //                     "refresh-revealed", "refresh-unwarranted":
    459 //                    break
    460                 default:
    461                     logger.error("NEW Notification: \(message)")        // this is a new notification I haven't seen before
    462                     break
    463             }
    464         } catch let error {
    465             logger.error("Error \(error) parsing notification: \(message)")
    466             postNotification(.GeneralError, userInfo: [NOTIFICATIONERROR: error])
    467         // TODO: if DevMode then should log into file for user
    468         }
    469     }
    470 
    471     /// wallet-core logs from its own thread, thus hop to the main actor before the console
    472     func handleLog(message: String) {
    473         guard isLogging else { return }         // don't flood the main queue when nobody looks
    474         DispatchQueue.main.async { [self] in
    475             handleLogM(message: message)
    476         }
    477     }
    478 
    479     @MainActor private func handleLogM(message: String) {
    480         if #available (iOS 16.0, *) {
    481             if isLogging {
    482                 let consoleManager = LCManager.shared
    483                 consoleManager.print(message)
    484             }
    485         }
    486     }
    487 
    488     @available(iOS 16.0, *)
    489     @MainActor func observe(json: String, type: String?, eventID: String?, timestamp: String) {
    490         let consoleManager = LCManager.shared
    491         if let type {
    492             if let eventID {
    493                 consoleManager.print("\(type)   \(eventID)")
    494             } else {
    495                 consoleManager.print(type)
    496             }
    497         }
    498         consoleManager.print("   \(timestamp)")
    499         if isObserving < 0 {
    500             consoleManager.print(json)
    501         }
    502         consoleManager.print("-   -   -")
    503     }
    504 
    505     /// A message we could not decode may still be the answer to a pending request.
    506     /// Fail that request, otherwise it would wait for an answer which never comes.
    507     private func failPendingRequest(_ messageData: Data, _ message: String) {
    508         guard let header = try? JSONDecoder().decode(ResponseHeader.self, from: messageData),
    509               header.type == "response" || header.type == "error",
    510               let requestId = header.id,
    511               let (timeSent, completion) = takeCompletion(requestId)
    512         else { return }
    513         logger.error("undecodable \(header.type, privacy: .public) for request \(requestId, privacy: .public)")
    514         completion(requestId, timeSent, message, nil, WalletCore.parseFailureError())
    515     }
    516 
    517     /// wallet-core calls this from its own thread, thus hop to the main actor
    518     func handleMessage(message: String) {
    519         DispatchQueue.main.async { [self] in
    520             handleMessageM(message: message)
    521         }
    522     }
    523 
    524     /// here not only responses, but also notifications from wallet-core will be received
    525     @MainActor private func handleMessageM(message: String) {
    526         do {
    527             guard let messageData = message.data(using: .utf8) else {
    528                 throw WalletBackendError.deserializationError
    529             }
    530             do {
    531                 let decoded = try JSONDecoder().decode(ResponseOrNotification.self, from: messageData)
    532                 switch decoded.type {
    533                     case "error":
    534                         symLog.log("\"id\":\(decoded.id ?? 0)  \(message)")
    535                         try handleError(decoded, message)
    536                     case "response":
    537 //                        symLog.log(message)
    538                         try handleResponse(decoded, message)
    539                     case "notification":
    540 //                        symLog.log(message)
    541                         try handleNotification(decoded.payload, message)
    542                     case "tunnelHttp":          // TODO: Handle tunnelHttp
    543                         symLog.log("Can't handle tunnelHttp: \(message)")    // TODO: .error
    544                         throw WalletBackendError.deserializationError
    545                     default:
    546                         symLog.log("Unknown response type: \(message)")    // TODO: .error
    547                         throw WalletBackendError.deserializationError
    548                 }
    549             } catch {   // e.g. a TalerErrorDetail from a remote server which doesn't decode
    550                 failPendingRequest(messageData, message)    // never leave a request hanging
    551                 throw error
    552             }
    553         } catch DecodingError.dataCorrupted(let context) {
    554             logger.error("\(context.debugDescription)")
    555         } catch DecodingError.keyNotFound(let key, let context) {
    556             logger.error("Key '\(key.stringValue)' not found:\(context.debugDescription)")
    557             logger.error("\(context.codingPath)")
    558         } catch DecodingError.valueNotFound(let value, let context) {
    559             logger.error("Value '\(value)' not found:\(context.debugDescription)")
    560             logger.error("\(context.codingPath)")
    561         } catch DecodingError.typeMismatch(let type, let context) {
    562             logger.error("Type '\(type)' mismatch:\(context.debugDescription)")
    563             logger.error("\(context.codingPath)")
    564         } catch let error {
    565             logger.error("\(error.localizedDescription)")
    566             // Anything that is not a DecodingError means we could not classify the
    567             // message at all (e.g. tunnelHttp, or an unknown "type"). A second
    568             // `catch` after this one would be unreachable, so notify from here.
    569             delegate?.walletBackendReceivedUnknownMessage(self, message: message)
    570         }
    571     }
    572     
    573     private func encodeAndSend(_ request: WalletBackendRequest, completionHandler: @escaping (UInt, Date, String?, Data?, TalerErrorDetail?) -> Void) {
    574         // Encode the request and send it to the backend.
    575         queue.async {
    576             let requestId = self.reserveRequestId()
    577             let sendTime = Date.now
    578             do {
    579                 let full = FullRequest(operation: request.operation, id: requestId, args: request.args)
    580 //          symLog.log(full)
    581                 let encoded = try JSONEncoder().encode(full)
    582                 guard let jsonString = String(data: encoded, encoding: .utf8) else { throw WalletBackendError.serializationError }
    583                     let args = try JSONEncoder().encode(request.args)
    584                     if let jsonArgs = String(data: args, encoding: .utf8) {
    585                         if request.operation == "getTransactionsV2" {
    586                             if self.logTransactions {
    587                                 self.logger.trace("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public)\(jsonArgs, privacy: .auto)")
    588                             }
    589                         } else {
    590                             self.logger.log("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public)\(jsonArgs, privacy: .auto)")
    591                         }
    592                     } else {    // should NEVER happen since the whole request was already successfully encoded and stringified
    593                         self.logger.log("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public) 🔴 Error: jsonArgs")
    594                     }
    595                 // register only after everything which can throw did succeed, but before
    596                 // sending - the answer may arrive before sendMessage() even returns
    597                 self.setCompletion(requestId, sendTime, completionHandler)
    598                 self.quickjs.sendMessage(message: jsonString)
    599 //              self.symLog.log(jsonString)
    600             } catch {       // call completion - nothing was registered, thus nobody else will
    601                 self.logger.error("\(error.localizedDescription)")
    602 //              self.symLog.log(error)
    603                 completionHandler(requestId, sendTime, nil, nil, WalletCore.serializeRequestError());
    604             }
    605         }
    606     }
    607 }
    608 // MARK: -  async / await function
    609 extension WalletCore {
    610     /// send async requests to wallet-core
    611     func sendFormattedRequest<T: WalletBackendFormattedRequest> (_ request: T, asJSON: Bool = false) async throws -> (T.Response, UInt) {
    612         let reqData = WalletBackendRequest(operation: request.operation,
    613                                            args: AnyEncodable(request.args()))
    614         return try await withCheckedThrowingContinuation { continuation in
    615             encodeAndSend(reqData) { [self] requestId, timeSent, message, result, error in
    616                 let timeUsed = Date.now - timeSent
    617                 let millisecs = timeUsed.milliseconds
    618                 if let error {
    619                     logger.error("Request \"id\":\(requestId, privacy: .public) failed after \(millisecs, privacy: .public) ms, error: \(error.code, privacy: .public)")
    620                 } else {
    621 #if DEBUG
    622                     if millisecs > (nativeDB ? 20 : 50) {
    623                         logger.info("Request \"id\":\(requestId, privacy: .public) took \(millisecs, privacy: .public) ms")
    624                     }
    625 #endif
    626                 }
    627                 var err: Error? = nil
    628                 if let json = result, error == nil {
    629                     do {
    630                         if asJSON {
    631                             if let message, let response = message as? T.Response {
    632                                 continuation.resume(returning: (response, requestId))
    633                             } else {
    634                                 continuation.resume(throwing: TransactionDecodingError.invalidStringValue)
    635                             }
    636                         } else {
    637                             let decoded = try JSONDecoder().decode(T.Response.self, from: json)
    638                             continuation.resume(returning: (decoded, requestId))
    639                         }
    640                         return
    641                     } catch DecodingError.dataCorrupted(let context) {
    642                         logger.error("\(context.debugDescription)")
    643                         err = DecodingError.dataCorrupted(context)
    644                     } catch DecodingError.keyNotFound(let key, let context) {
    645                         logger.error("Key '\(key.stringValue)' not found:\(context.debugDescription)")
    646                         logger.error("\(context.codingPath)")
    647                         err = DecodingError.keyNotFound(key, context)
    648                     } catch DecodingError.valueNotFound(let value, let context) {
    649                         logger.error("Value '\(value)' not found:\(context.debugDescription)")
    650                         logger.error("\(context.codingPath)")
    651                         err = DecodingError.valueNotFound(value, context)
    652                     } catch DecodingError.typeMismatch(let type, let context) {
    653                         logger.error("Type '\(type)' mismatch:\(context.debugDescription)")
    654                         logger.error("\(context.codingPath)")
    655                         err = DecodingError.typeMismatch(type, context)
    656                     } catch {       // rethrows
    657                         if let jsonString = String(data: json, encoding: .utf8) {
    658                             symLog.log(jsonString)       // TODO: .error
    659                         } else {
    660                             symLog.log(json)       // TODO: .error
    661                         }
    662                         err = error     // this will be thrown in continuation.resume(throwing:), otherwise keep nil
    663                     }
    664                 } else if let error {
    665                     // TODO: WALLET_CORE_REQUEST_CANCELLED
    666                     lastError = FullError(type: "error", operation: request.operation, id: requestId, error: error)
    667                     err = WalletBackendError.walletCoreError(error)
    668                 } else {        // both result and error are nil
    669                     lastError = nil
    670                 }
    671                 continuation.resume(throwing: err ?? TransactionDecodingError.invalidStringValue)
    672             }
    673         }
    674     }
    675 }