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