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 }