taler-ios

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

WalletCore.swift (29116B)


      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?, WalletBackendResponseError?) -> Void)] = [:]
     33     var delegate: WalletBackendDelegate?
     34 
     35     var versionInfo: VersionInfo?           // shown in SettingsView
     36     var developDelay: Bool?                 // if set in SettingsView will delay wallet-core after each action
     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: WalletBackendResponseError
     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: WalletBackendResponseError?
     72         let payload: AnyCodable?
     73     }
     74 
     75     struct Payload: Decodable {
     76         let type: String
     77         let id: String?
     78         let reservePub: String?
     79         let isInternal: Bool?
     80         let hintTransactionId: String?
     81         let event: [String: AnyCodable]?
     82     }
     83 
     84     deinit {
     85         logger.log("shutdown Quickjs")
     86     // TODO: send shutdown message to talerWalletInstance
     87 //        quickjs.waitStopped()
     88     }
     89 
     90     init() throws {
     91         isObserving = 0
     92         isLogging = false
     93         logTransactions = false
     94 //        logger.trace("init Quickjs")
     95         requestsMade = 0
     96         queue = DispatchQueue(label: "net.taler.myQueue", attributes: .concurrent)
     97         semaphore = DispatchSemaphore(value: 1)
     98         quickjs = Quickjs()
     99         quickjs.messageHandler = self
    100         logger.log("Quickjs running")
    101     }
    102 }
    103 // MARK: -  completionHandler functions
    104 extension WalletCore {
    105     private func handleError(_ decoded: ResponseOrNotification, _ message: String?) throws {
    106         guard let requestId = decoded.id else {
    107             logger.error("didn't find requestId in error response")
    108             // TODO: show error alert
    109             throw WalletBackendError.deserializationError
    110         }
    111         guard let (timeSent, completion) = completions[requestId] else {
    112             logger.error("requestId \(requestId, privacy: .public) not in list")
    113             // TODO: show error alert
    114             throw WalletBackendError.deserializationError
    115         }
    116         completions[requestId] = nil
    117         if let walletError = decoded.error {            // wallet-core sent an error message
    118             do {
    119                 let jsonData = try JSONEncoder().encode(walletError)
    120                 let responseCode = walletError.errorResponse?.code ?? 0
    121                 logger.error("wallet-core sent back error \(walletError.code, privacy: .public), \(responseCode, privacy: .public) for request \(requestId, privacy: .public)")
    122                 symLog.log("id:\(requestId)  \(walletError)")
    123                 completion(requestId, timeSent, message, jsonData, walletError)
    124             } catch {        // JSON encoding of response.result failed / should never happen
    125                 symLog.log(decoded)
    126                 logger.error("cannot encode wallet-core Error")
    127                 completion(requestId, timeSent, message, nil, WalletCore.parseFailureError())
    128             }
    129         } else {             // JSON decoding of error message failed
    130             completion(requestId, timeSent, message, nil, WalletCore.parseFailureError())
    131         }
    132     }
    133 
    134     private func handleResponse(_ decoded: ResponseOrNotification, _ message: String) throws {
    135         guard let requestId = decoded.id else {
    136             logger.error("didn't find requestId in response")
    137             symLog.log(decoded)                 // TODO: .error
    138             throw WalletBackendError.deserializationError
    139         }
    140         guard let (timeSent, completion) = completions[requestId] else {
    141             logger.error("requestId \(requestId, privacy: .public) not in list")
    142             throw WalletBackendError.deserializationError
    143         }
    144         completions[requestId] = nil
    145         guard let result = decoded.result else {
    146             logger.error("requestId \(requestId, privacy: .public) got no result")
    147             throw WalletBackendError.deserializationError
    148         }
    149         do {
    150             let jsonData = try JSONEncoder().encode(result)
    151             if let operation = decoded.operation {
    152                 if operation == "getTransactionsV2" {
    153                     if logTransactions {
    154                         symLog.log(message)
    155                     }
    156                 } else {
    157                     symLog.log(message)
    158                 }
    159             }
    160 //            logger.info(result)   TODO: log result
    161             completion(requestId, timeSent, message, jsonData, nil)
    162         } catch {        // JSON encoding of response.result failed / should never happen
    163             symLog.log(result)                 // TODO: .error
    164             completion(requestId, timeSent, message, nil, WalletCore.parseResponseError())
    165         }
    166     }
    167 
    168     @MainActor
    169     private func postNotificationM(_ aName: NSNotification.Name,
    170                            object anObject: Any? = nil,
    171                                   userInfo: [AnyHashable: Any]? = nil) async {
    172         NotificationCenter.default.post(name: aName, object: anObject, userInfo: userInfo)
    173     }
    174     private func postNotification(_ aName: NSNotification.Name,
    175                           object anObject: Any? = nil,
    176                                  userInfo: [AnyHashable: Any]? = nil) {
    177         Task { // runs on MainActor
    178             await postNotificationM(aName, object: anObject, userInfo: userInfo)
    179 //            logger.info("Notification sent: \(aName.rawValue, privacy: .public)")
    180         }
    181     }
    182 
    183     private func handlePendingProcessed(_ payload: Payload) throws {
    184         guard let id = payload.id else {
    185             throw WalletBackendError.deserializationError
    186         }
    187         let pendingOp = Notification.Name.PendingOperationProcessed.rawValue
    188         if id.hasPrefix("exchange-update:") {
    189             // Bla Bla Bla
    190         } else if id.hasPrefix("refresh:") {
    191             // Bla Bla Bla
    192         } else if id.hasPrefix("purchase:") {
    193             // TODO: handle purchase
    194 //            symLog.log("\(pendingOp): \(id)")
    195         } else if id.hasPrefix("withdraw:") {
    196             // TODO: handle withdraw
    197 //            symLog.log("\(pendingOp): \(id)")
    198         } else if id.hasPrefix("peer-pull-credit:") {
    199             // TODO: handle peer-pull-credit
    200 //            symLog.log("\(pendingOp): \(id)")
    201         } else if id.hasPrefix("peer-push-debit:") {
    202             // TODO: handle peer-push-debit
    203 //            symLog.log("\(pendingOp): \(id)")
    204         } else {
    205             // TODO: handle other pending-operation-processed
    206             logger.log("❗️ \(pendingOp, privacy: .public): \(id, privacy: .public)")        // this is a new pendingOp I haven't seen before
    207         }
    208     }
    209     @MainActor private func handleStateTransition(_ jsonData: Data) throws {
    210         do {
    211             let decoded = try JSONDecoder().decode(TransactionTransition.self, from: jsonData)
    212             if let errorInfo = decoded.errorInfo {
    213                 // reload pending transaction list to add error badge
    214                 postNotification(.TransactionError, userInfo: [NOTIFICATIONERROR: WalletBackendError.walletCoreError(errorInfo)])
    215             }
    216             guard decoded.newTxState != decoded.oldTxState else {
    217                 logger.info("handleStateTransition: No State change: \(decoded.transactionId, privacy: .private(mask: .hash))")
    218                 return
    219             }
    220 
    221             let components = decoded.transactionId.components(separatedBy: ":")
    222             if components.count >= 3 {  // txn:$txtype:$uid
    223                 if let type = TransactionType(rawValue: components[1]) {
    224                     guard type != .refresh else { return }
    225                     let newMajor = decoded.newTxState.major
    226                     let newMinor = decoded.newTxState.minor
    227                     let oldMinor = decoded.oldTxState?.minor
    228                     switch newMajor {
    229                         case .done:
    230                             logger.info("handleStateTransition: Done: \(decoded.transactionId, privacy: .private(mask: .hash))")
    231                             if type.isWithdrawal {
    232                                 Controller.shared.playSound(2)  // play payment_received only for withdrawals
    233                             } else if !type.isIncoming {
    234                                 if !(oldMinor == .autoRefund || oldMinor == .acceptRefund) {
    235                                     Controller.shared.playSound(1)  // play payment_sent for all outgoing tx
    236                                 }
    237                             } else {    // incoming but not withdrawal
    238                                 logger.info("    incoming payment done - NO sound - \(type.rawValue)")
    239                             }
    240                             postNotification(.TransactionDone, userInfo: [TRANSACTIONTRANSITION: decoded])
    241                             return
    242                         case .aborting:
    243                             logger.log("handleStateTransition: Aborting: \(decoded.transactionId, privacy: .private(mask: .hash))")
    244                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    245                         case .expired:
    246                             logger.warning("handleStateTransition: Expired: \(decoded.transactionId, privacy: .private(mask: .hash))")
    247                             if let index = expired.firstIndex(of: components[2]) {
    248                                 expired.remove(at: index)                       // don't beep twice
    249                             } else {
    250                                 expired.append(components[2])
    251                                 Controller.shared.playSound(0)                  // beep at first sight
    252                             }
    253                             postNotification(.TransactionExpired, userInfo: [TRANSACTIONTRANSITION: decoded])
    254                         case .pending:
    255                             if let newMinor {
    256                                 if newMinor == .ready {
    257                                     logger.log("handleStateTransition: PendingReady: \(decoded.transactionId, privacy: .private(mask: .hash))")
    258                                     postNotification(.PendingReady, userInfo: [TRANSACTIONTRANSITION: decoded])
    259                                     return
    260                                 } else if newMinor == .exchangeWaitReserve      // user did confirm on bank website
    261                                        || newMinor == .withdraw {               // coin-withdrawal has started
    262 //                                    logger.log("DismissSheet: \(decoded.transactionId, privacy: .private(mask: .hash))")
    263                                     postNotification(.DismissSheet, userInfo: [TRANSACTIONTRANSITION: decoded])
    264                                     return
    265                                 } else if newMinor == .kyc {       // user did confirm on bank website, but KYC is needed
    266                                     logger.log("handleStateTransition: KYCrequired: \(decoded.transactionId, privacy: .private(mask: .hash))")
    267                                     postNotification(.KYCrequired, userInfo: [TRANSACTIONTRANSITION: decoded])
    268                                     return
    269                                 }
    270                                 logger.trace("handleStateTransition: Pending:\(newMinor.rawValue, privacy: .public) \(decoded.transactionId, privacy: .private(mask: .hash))")
    271                             } else {
    272                                 logger.trace("handleStateTransition: Pending: \(decoded.transactionId, privacy: .private(mask: .hash))")
    273                             }
    274                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    275                         default:
    276                             if let newMinor {
    277                                 logger.log("handleStateTransition: \(newMajor.rawValue, privacy: .public):\(newMinor.rawValue, privacy: .public) \(decoded.transactionId, privacy: .private(mask: .hash))")
    278                             } else {
    279                                 logger.warning("handleStateTransition: \(newMajor.rawValue, privacy: .public): \(decoded.transactionId, privacy: .private(mask: .hash))")
    280                             }
    281                             postNotification(.TransactionStateTransition, userInfo: [TRANSACTIONTRANSITION: decoded])
    282                     } // switch
    283                 } // type
    284             } // 3 components
    285             return
    286         } catch DecodingError.dataCorrupted(let context) {
    287             logger.error("handleStateTransition: \(context.debugDescription)")
    288         } catch DecodingError.keyNotFound(let key, let context) {
    289             logger.error("handleStateTransition: Key '\(key.stringValue)' not found:\(context.debugDescription)")
    290             logger.error("\(context.codingPath)")
    291         } catch DecodingError.valueNotFound(let value, let context) {
    292             logger.error("handleStateTransition: Value '\(value)' not found:\(context.debugDescription)")
    293             logger.error("\(context.codingPath)")
    294         } catch DecodingError.typeMismatch(let type, let context) {
    295             logger.error("handleStateTransition: Type '\(type)' mismatch:\(context.debugDescription)")
    296             logger.error("\(context.codingPath)")
    297         } catch let error {       // rethrows
    298             logger.error("handleStateTransition: \(error.localizedDescription)")
    299         }
    300         throw WalletBackendError.walletCoreError(nil)       // TODO: error?
    301     }
    302 
    303     @MainActor private func handleNotification(_ anyCodable: AnyCodable?, _ message: String) throws {
    304         guard let anyPayload = anyCodable else {
    305             throw WalletBackendError.deserializationError
    306         }
    307         do {
    308             let jsonData = try JSONEncoder().encode(anyPayload)
    309             let payload = try JSONDecoder().decode(Payload.self, from: jsonData)
    310 
    311             switch payload.type {
    312                 case Notification.Name.Idle.rawValue:
    313 //                    symLog.log(message)
    314                     break
    315                 case Notification.Name.ExchangeStateTransition.rawValue:
    316                     symLog.log(message)
    317                     break
    318                 case Notification.Name.TransactionStateTransition.rawValue:
    319                     symLog.log(message)
    320                     try handleStateTransition(jsonData)
    321                 case Notification.Name.PendingOperationProcessed.rawValue:
    322                     try handlePendingProcessed(payload)
    323                 case Notification.Name.BalanceChange.rawValue:
    324                     let now = Date()
    325                     symLog.log(message)
    326                     if !(payload.isInternal ?? false) {     // don't re-post internals
    327                         if let txID = payload.hintTransactionId {
    328                             if txID.contains("txn:refresh:") {
    329                                 break                       // don't re-post refresh
    330                             }
    331                         }
    332                         postNotification(.BalanceChange, userInfo: [NOTIFICATIONTIME: now])
    333                     }
    334                 case Notification.Name.BankAccountChange.rawValue:
    335                     symLog.log(message)
    336                     postNotification(.BankAccountChange)
    337                 case Notification.Name.ExchangeAdded.rawValue:
    338                     symLog.log(message)
    339                     postNotification(.ExchangeAdded)
    340                 case Notification.Name.ExchangeDeleted.rawValue:
    341                     symLog.log(message)
    342                     postNotification(.ExchangeDeleted)
    343                 case Notification.Name.ReserveNotYetFound.rawValue:
    344                     if let reservePub = payload.reservePub {
    345                         let userInfo = ["reservePub" : reservePub]
    346 //                        postNotification(.ReserveNotYetFound, userInfo: userInfo)   // TODO: remind User to confirm withdrawal
    347                     } // else { throw WalletBackendError.deserializationError }   shouldn't happen, but if it does just ignore it
    348 
    349                 case Notification.Name.ProposalAccepted.rawValue:               // "proposal-accepted":
    350                     symLog.log(message)
    351                     postNotification(.ProposalAccepted, userInfo: nil)
    352                 case Notification.Name.ProposalDownloaded.rawValue:             // "proposal-downloaded":
    353                     symLog.log(message)
    354                     postNotification(.ProposalDownloaded, userInfo: nil)
    355                 case Notification.Name.TaskObservabilityEvent.rawValue,
    356                      Notification.Name.RequestObservabilityEvent.rawValue:
    357                     if isObserving != 0 {
    358                         symLog.log(message)
    359                         let timestamp = TalerDater.dateString()
    360                         if let event = payload.event, let json = event.toJSON() {
    361                             let type = event["type"]?.value as? String
    362                             let eventID = event["id"]?.value as? String
    363                             if #available(iOS 16.0, *) {
    364                                 observe(json: json,
    365                                         type: type,
    366                                      eventID: eventID,
    367                                    timestamp: timestamp)
    368                             }
    369                         }
    370                     }
    371                     // TODO: remove these once wallet-core doesn't send them anymore
    372 //                case "refresh-started", "refresh-melted",
    373 //                     "refresh-revealed", "refresh-unwarranted":
    374 //                    break
    375                 default:
    376                     logger.error("NEW Notification: \(message)")        // this is a new notification I haven't seen before
    377                     break
    378             }
    379         } catch let error {
    380             logger.error("Error \(error) parsing notification: \(message)")
    381             postNotification(.GeneralError, userInfo: [NOTIFICATIONERROR: error])
    382         // TODO: if DevMode then should log into file for user
    383         }
    384     }
    385 
    386     @MainActor func handleLog(message: String) {
    387         if #available (iOS 16.0, *) {
    388             if isLogging {
    389                 let consoleManager = LCManager.shared
    390                 consoleManager.print(message)
    391             }
    392         }
    393     }
    394 
    395     @available(iOS 16.0, *)
    396     @MainActor func observe(json: String, type: String?, eventID: String?, timestamp: String) {
    397         let consoleManager = LCManager.shared
    398         if let type {
    399             if let eventID {
    400                 consoleManager.print("\(type)   \(eventID)")
    401             } else {
    402                 consoleManager.print(type)
    403             }
    404         }
    405         consoleManager.print("   \(timestamp)")
    406         if isObserving < 0 {
    407             consoleManager.print(json)
    408         }
    409         consoleManager.print("-   -   -")
    410     }
    411 
    412     /// here not only responses, but also notifications from wallet-core will be received
    413     @MainActor func handleMessage(message: String) {
    414         do {
    415             var asyncDelay = 0
    416             if let delay: Bool = developDelay {   // Settings: 2 seconds delay
    417                 if delay {
    418                     asyncDelay = 2
    419                 }
    420             }
    421             if asyncDelay > 0 {
    422                 symLog.log(message)
    423                 symLog.log("...going to sleep for \(asyncDelay) seconds...")
    424                 sleep(UInt32(asyncDelay))
    425                 symLog.log("waking up again after \(asyncDelay) seconds, will deliver message")
    426             }
    427             guard let messageData = message.data(using: .utf8) else {
    428                 throw WalletBackendError.deserializationError
    429             }
    430             let decoded = try JSONDecoder().decode(ResponseOrNotification.self, from: messageData)
    431             switch decoded.type {
    432                 case "error":
    433                     symLog.log("\"id\":\(decoded.id ?? 0)  \(message)")
    434                     try handleError(decoded, message)
    435                 case "response":
    436 //                    symLog.log(message)
    437                     try handleResponse(decoded, message)
    438                 case "notification":
    439 //                    symLog.log(message)
    440                     try handleNotification(decoded.payload, message)
    441                 case "tunnelHttp":          // TODO: Handle tunnelHttp
    442                     symLog.log("Can't handle tunnelHttp: \(message)")    // TODO: .error
    443                     throw WalletBackendError.deserializationError
    444                 default:
    445                     symLog.log("Unknown response type: \(message)")    // TODO: .error
    446                     throw WalletBackendError.deserializationError
    447             }
    448         } catch DecodingError.dataCorrupted(let context) {
    449             logger.error("\(context.debugDescription)")
    450         } catch DecodingError.keyNotFound(let key, let context) {
    451             logger.error("Key '\(key.stringValue)' not found:\(context.debugDescription)")
    452             logger.error("\(context.codingPath)")
    453         } catch DecodingError.valueNotFound(let value, let context) {
    454             logger.error("Value '\(value)' not found:\(context.debugDescription)")
    455             logger.error("\(context.codingPath)")
    456         } catch DecodingError.typeMismatch(let type, let context) {
    457             logger.error("Type '\(type)' mismatch:\(context.debugDescription)")
    458             logger.error("\(context.codingPath)")
    459         } catch let error {
    460             logger.error("\(error.localizedDescription)")
    461         } catch { // TODO: ?
    462             delegate?.walletBackendReceivedUnknownMessage(self, message: message)
    463         }
    464     }
    465     
    466     private func encodeAndSend(_ request: WalletBackendRequest, completionHandler: @escaping (UInt, Date, String?, Data?, WalletBackendResponseError?) -> Void) {
    467         // Encode the request and send it to the backend.
    468         queue.async {
    469             self.semaphore.wait()               // guard access to requestsMade
    470             let requestId = self.requestsMade
    471             let sendTime = Date.now
    472             do {
    473                 let full = FullRequest(operation: request.operation, id: requestId, args: request.args)
    474 //          symLog.log(full)
    475                 let encoded = try JSONEncoder().encode(full)
    476                 guard let jsonString = String(data: encoded, encoding: .utf8) else { throw WalletBackendError.serializationError }
    477                 self.completions[requestId] = (sendTime, completionHandler)
    478                 self.requestsMade += 1
    479                 self.semaphore.signal()         // free requestsMade
    480                     let args = try JSONEncoder().encode(request.args)
    481                     if let jsonArgs = String(data: args, encoding: .utf8) {
    482                         if request.operation == "getTransactionsV2" {
    483                             if self.logTransactions {
    484                                 self.logger.trace("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public)\(jsonArgs, privacy: .auto)")
    485                             }
    486                         } else {
    487                             self.logger.log("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public)\(jsonArgs, privacy: .auto)")
    488                         }
    489                     } else {    // should NEVER happen since the whole request was already successfully encoded and stringified
    490                         self.logger.log("🔴\"id\":\(requestId, privacy: .public) \(request.operation, privacy: .public) 🔴 Error: jsonArgs")
    491                     }
    492                 self.quickjs.sendMessage(message: jsonString)
    493 //              self.symLog.log(jsonString)
    494             } catch {       // call completion
    495                 self.semaphore.signal()         // free requestsMade
    496                 self.logger.error("\(error.localizedDescription)")
    497 //              self.symLog.log(error)
    498                 completionHandler(requestId, sendTime, nil, nil, WalletCore.serializeRequestError());
    499             }
    500         }
    501     }
    502 }
    503 // MARK: -  async / await function
    504 extension WalletCore {
    505     /// send async requests to wallet-core
    506     func sendFormattedRequest<T: WalletBackendFormattedRequest> (_ request: T, asJSON: Bool = false) async throws -> (T.Response, UInt) {
    507         let reqData = WalletBackendRequest(operation: request.operation(),
    508                                            args: AnyEncodable(request.args()))
    509         return try await withCheckedThrowingContinuation { continuation in
    510             encodeAndSend(reqData) { [self] requestId, timeSent, message, result, error in
    511                 let timeUsed = Date.now - timeSent
    512                 let millisecs = timeUsed.milliseconds
    513                 if let error {
    514                     logger.error("Request \"id\":\(requestId, privacy: .public) failed after \(millisecs, privacy: .public) ms")
    515                 } else {
    516                     if millisecs > 50 {
    517                         logger.info("Request \"id\":\(requestId, privacy: .public) took \(millisecs, privacy: .public) ms")
    518                     }
    519                 }
    520                 var err: Error? = nil
    521                 if let json = result, error == nil {
    522                     do {
    523                         if asJSON {
    524                             if let message {
    525                                 continuation.resume(returning: (message as! T.Response, requestId))
    526                             } else {
    527                                 continuation.resume(throwing: TransactionDecodingError.invalidStringValue)
    528                             }
    529                         } else {
    530                             let decoded = try JSONDecoder().decode(T.Response.self, from: json)
    531                             continuation.resume(returning: (decoded, requestId))
    532                         }
    533                         return
    534                     } catch DecodingError.dataCorrupted(let context) {
    535                         logger.error("\(context.debugDescription)")
    536                     } catch DecodingError.keyNotFound(let key, let context) {
    537                         logger.error("Key '\(key.stringValue)' not found:\(context.debugDescription)")
    538                         logger.error("\(context.codingPath)")
    539                     } catch DecodingError.valueNotFound(let value, let context) {
    540                         logger.error("Value '\(value)' not found:\(context.debugDescription)")
    541                         logger.error("\(context.codingPath)")
    542                     } catch DecodingError.typeMismatch(let type, let context) {
    543                         logger.error("Type '\(type)' mismatch:\(context.debugDescription)")
    544                         logger.error("\(context.codingPath)")
    545                     } catch {       // rethrows
    546                         if let jsonString = String(data: json, encoding: .utf8) {
    547                             symLog.log(jsonString)       // TODO: .error
    548                         } else {
    549                             symLog.log(json)       // TODO: .error
    550                         }
    551                         err = error     // this will be thrown in continuation.resume(throwing:), otherwise keep nil
    552                     }
    553                 } else if let error {
    554                     // TODO: WALLET_CORE_REQUEST_CANCELLED
    555                     lastError = FullError(type: "error", operation: request.operation(), id: requestId, error: error)
    556                     err = WalletBackendError.walletCoreError(error)
    557                 } else {        // both result and error are nil
    558                     lastError = nil
    559                 }
    560                 continuation.resume(throwing: err ?? TransactionDecodingError.invalidStringValue)
    561             }
    562         }
    563     }
    564 }