WalletModel.swift (27979B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import Foundation 9 import taler_swift 10 import SymLog 11 import os.log 12 13 enum InsufficientBalanceHint: String, Codable { 14 /// Merchant doesn't accept money from exchange(s) that the wallet supports 15 case merchantAcceptInsufficient = "merchant-accept-insufficient" 16 /// Merchant accepts funds from a matching exchange, but the funds can't be deposited with the wire method 17 case merchantDepositInsufficient = "merchant-deposit-insufficient" 18 /// While in principle the balance is sufficient, the age restriction on coins causes the spendable balance to be insufficient 19 case ageRestricted = "age-restricted" 20 /// Wallet has enough available funds, but the material funds are insufficient 21 /// Usually because there is a pending refresh operation 22 case walletBalanceMaterialInsufficient = "wallet-balance-material-insufficient" 23 /// The wallet simply doesn't have enough available funds 24 case walletBalanceAvailableInsufficient = "wallet-balance-available-insufficient" 25 /// Exchange is missing the global fee configuration, thus fees are unknown 26 /// and funds from this exchange can't be used for p2p payments 27 case exchangeMissingGlobalFees = "exchange-missing-global-fees" 28 /// Even though the balance looks sufficient for the instructed amount, 29 /// the fees can be covered by neither the merchant nor the remaining wallet balance 30 case feesNotCovered = "fees-not-covered" 31 /// wallet-core keeps adding hints (feesNotCovered and exchangeMissingGlobalFees were 32 /// added after this enum was written). Throwing on the next one would take the whole 33 /// error message down with it - and an error that fails to decode never resumes the 34 /// Task waiting for that request. 35 case unknown 36 37 init(from decoder: Decoder) throws { 38 let raw = try decoder.singleValueContainer().decode(String.self) 39 self = InsufficientBalanceHint(rawValue: raw) ?? .unknown 40 } 41 42 func localizedCause(_ currency: String) -> String { 43 switch self { 44 case .merchantAcceptInsufficient: 45 String(localized: "payment_balance_insufficient_hint_merchant_accept_insufficient", 46 defaultValue: "Merchant doesn't accept money from one or more providers in this wallet") 47 case .merchantDepositInsufficient: 48 String(localized: "payment_balance_insufficient_hint_merchant_deposit_insufficient", 49 defaultValue: "Merchant doesn't accept the wire method of the provider, this likely means it is misconfigured") 50 case .ageRestricted: 51 String(localized: "payment_balance_insufficient_hint_age_restricted", 52 defaultValue: "Purchase not possible due to age restriction") 53 case .walletBalanceMaterialInsufficient: 54 String(localized: "payment_balance_insufficient_hint_wallet_balance_material_insufficient", 55 defaultValue: "Some of the digital cash needed for this purchase is currently unavailable") 56 case .walletBalanceAvailableInsufficient: 57 String(localized: "payment_balance_insufficient_max", 58 defaultValue: "Balance insufficient! You don't have enough \(currency).") 59 case .exchangeMissingGlobalFees: 60 String(localized: "payment_balance_insufficient_hint_exchange_missing_global_fees", 61 defaultValue: "Provider is missing the global fee configuration, this likely means it is misconfigured") 62 case .feesNotCovered: 63 String(localized: "payment_balance_insufficient_hint_fees_not_covered", 64 defaultValue: "Not enough funds to pay the provider fees not covered by the merchant") 65 case .unknown: // we don't know the reason, but we do know the balance is insufficient 66 String(localized: "payment_balance_insufficient_max", 67 defaultValue: "Balance insufficient! You don't have enough \(currency).") 68 } 69 } 70 } 71 72 struct InsufficientBalanceDetailsPerExchange: Codable, Hashable { 73 var balanceAvailable: Amount 74 var balanceMaterial: Amount 75 var balanceExchangeDepositable: Amount 76 var balanceAgeAcceptable: Amount 77 var balanceReceiverAcceptable: Amount? // deprecated 78 var balanceReceiverDepositable: Amount 79 var maxEffectiveSpendAmount: Amount 80 /// Exchange doesn't have global fees configured for the relevant year, p2p payments aren't possible. 81 var missingGlobalFees: Bool? // deprecated 82 } 83 84 /// Detailed reason for why the wallet's balance is insufficient. 85 struct PaymentInsufficientBalanceDetails: Codable, Hashable { 86 /// Amount requested by the merchant. 87 var amountRequested: Amount 88 var causeHint: InsufficientBalanceHint? 89 /// Balance of type "available" (see balance.ts for definition). 90 var balanceAvailable: Amount 91 /// Balance of type "material" (see balance.ts for definition). 92 var balanceMaterial: Amount 93 /// Balance of type "age-acceptable" (see balance.ts for definition). 94 var balanceAgeAcceptable: Amount 95 /// Balance of type "merchant-acceptable" (see balance.ts for definition). 96 var balanceReceiverAcceptable: Amount 97 /// Balance of type ... 98 var balanceReceiverDepositable: Amount 99 var balanceExchangeDepositable: Amount 100 /// Maximum effective amount that the wallet can spend, when all fees are paid by the wallet. 101 var maxEffectiveSpendAmount: Amount 102 var perExchange: [String : InsufficientBalanceDetailsPerExchange] 103 } 104 // MARK: - 105 struct TalerErrorInfo: Codable, Hashable { 106 /// Numeric error code defined in the GANA gnu-taler-error-codes registry. 107 /// Optional: this is the response of some *other* server, which need not 108 /// be a Taler error object at all. 109 var code: Int? 110 // all other fields are optional: 111 var when: Timestamp? 112 /// English description of the error code. 113 var hint: String? 114 var message: String? 115 } 116 struct TalerErrorDetail: Codable, Hashable { 117 /// Numeric error code defined in the GANA gnu-taler-error-codes registry. 118 var code: Int 119 // all other fields are optional: 120 var when: Timestamp? 121 /// English description of the error code. 122 var hint: String? 123 124 /// Error details, type depends on `talerErrorCode`. 125 var detail: String? 126 127 /// HTTPError 128 var requestUrl: String? 129 var requestMethod: String? 130 var httpStatusCode: Int? 131 var stack: String? 132 var errorResponse: TalerErrorInfo? 133 134 var insufficientBalanceDetails: PaymentInsufficientBalanceDetails? 135 136 enum CodingKeys: String, CodingKey { 137 case code, when, hint, detail 138 case requestUrl, requestMethod, httpStatusCode, stack 139 case errorResponse, insufficientBalanceDetails 140 } 141 } 142 extension TalerErrorDetail { 143 /// In wallet-core a TalerErrorDetail is an open dictionary: besides code, when and 144 /// hint it carries whatever the throwing code put there, and the shape of that 145 /// differs per error code - `detail`, for instance, is a string for some codes and a 146 /// nested error object for others. 147 /// Throwing here is not survivable: the error arrives inside the top-level message 148 /// envelope, so if it doesn't decode then WalletCore never finds the request id and 149 /// the continuation awaiting that request is never resumed - the sheet spins forever. 150 /// Therefore decode the optional extras defensively and drop what we cannot read. 151 init(from decoder: Decoder) throws { 152 let container = try decoder.container(keyedBy: CodingKeys.self) 153 code = try container.decode(Int.self, forKey: .code) 154 when = try? container.decodeIfPresent(Timestamp.self, forKey: .when) 155 hint = try? container.decodeIfPresent(String.self, forKey: .hint) 156 detail = try? container.decodeIfPresent(String.self, forKey: .detail) 157 requestUrl = try? container.decodeIfPresent(String.self, forKey: .requestUrl) 158 requestMethod = try? container.decodeIfPresent(String.self, forKey: .requestMethod) 159 httpStatusCode = try? container.decodeIfPresent(Int.self, forKey: .httpStatusCode) 160 stack = try? container.decodeIfPresent(String.self, forKey: .stack) 161 errorResponse = try? container.decodeIfPresent(TalerErrorInfo.self, forKey: .errorResponse) 162 insufficientBalanceDetails = try? container.decodeIfPresent(PaymentInsufficientBalanceDetails.self, 163 forKey: .insufficientBalanceDetails) 164 } 165 } 166 // MARK: - 167 /// Communicate with wallet-core 168 final class WalletModel: ObservableObject, Sendable { 169 public static let shared = WalletModel() 170 let logger = Logger(subsystem: "net.taler.gnu", category: "WalletModel") 171 172 @Published var error2: ErrorData? = nil 173 174 @MainActor func setError(_ theError: Error?) { 175 if let theError { 176 self.error2 = .error(theError) 177 } else { 178 self.error2 = nil 179 } 180 } 181 @MainActor func setMessage(_ title: String,_ theMessage: String?) { 182 if let theMessage { 183 self.error2 = .message(title: title, message: theMessage) 184 } else { 185 self.error2 = nil 186 } 187 } 188 189 func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, viewHandles: Bool = false, asJSON: Bool = false) 190 async throws -> (T.Response, UInt) { // T for any Thread 191 #if !DEBUG 192 logger.log("sending: \(request.operation, privacy: .public)") 193 #endif 194 let sendTime = Date.now 195 do { 196 let (response, id) = try await WalletCore.shared.sendFormattedRequest(request, asJSON: asJSON) 197 #if !DEBUG 198 let timeUsed = Date.now - sendTime 199 logger.log("received: \(request.operation, privacy: .public) (\(id, privacy: .public)) after \(timeUsed.milliseconds, privacy: .public) ms") 200 #endif 201 return (response, id) 202 } catch { // rethrows 203 let timeUsed = Date.now - sendTime 204 logger.error("\(request.operation, privacy: .public) failed after \(timeUsed.milliseconds, privacy: .public) ms\n\(error, privacy: .public)") 205 if !viewHandles { 206 // TODO: symlog + controller sound 207 await setError(error) 208 } 209 throw error 210 } 211 } 212 } 213 // MARK: - 214 struct DbStatus: Decodable, Sendable { 215 var dbReadHealthy: Bool 216 var dbWriteHealthy: Bool 217 } 218 /// A request to tell wallet-core about the network. 219 fileprivate struct ApplicationResumedRequest: WalletBackendFormattedRequest { 220 typealias Response = DbStatus 221 var operation: String { "hintApplicationResumed" } 222 func args() -> Args { Args() } 223 224 struct Args: Encodable {} // no arguments needed 225 } 226 227 fileprivate struct NetworkAvailabilityRequest: WalletBackendFormattedRequest { 228 struct Response: Decodable {} 229 var operation: String { "hintNetworkAvailability" } 230 func args() -> Args { Args(isNetworkAvailable: isNetworkAvailable) } 231 232 var isNetworkAvailable: Bool 233 234 struct Args: Encodable { 235 var isNetworkAvailable: Bool 236 } 237 } 238 239 extension WalletModel { 240 func hintNetworkAvailabilityT(_ isNetworkAvailable: Bool = false) async { 241 // T for any Thread 242 let request = NetworkAvailabilityRequest(isNetworkAvailable: isNetworkAvailable) 243 _ = try? await sendRequest(request) 244 } 245 func hintApplicationResumedT() async throws -> DbStatus { 246 // T for any Thread 247 let request = ApplicationResumedRequest() 248 let (response, id) = try await sendRequest(request) 249 return response 250 } 251 } 252 // MARK: - 253 struct WaitTransactionResult: Decodable, Sendable { 254 var matched: String? 255 var txState: TransactionState? 256 } 257 258 /// A request to wait for a background wallet transaction. 259 fileprivate struct WaitTransactionState: WalletBackendFormattedRequest { 260 typealias Response = WaitTransactionResult 261 var operation: String { "waitTransactionState" } 262 func args() -> Args { Args(transactionId: txId, txState: txState, bailStates: "final", progressToken: token) } 263 var txId: String 264 var txState: TransactionState 265 var token: String 266 267 struct Args: Encodable { 268 var transactionId: String 269 var txState: TransactionState 270 var bailStates: String 271 var progressToken: String 272 } 273 } 274 // MARK: - 275 /// A request to cancel a wallet transaction by token. 276 fileprivate struct CancelProgressToken: WalletBackendFormattedRequest { 277 struct Response: Decodable {} 278 var operation: String { "cancelProgressToken" } 279 func args() -> Args { Args(operation: op, progressToken: token) } 280 281 var op: String 282 var token: String 283 284 struct Args: Encodable { 285 var operation: String 286 var progressToken: String 287 } 288 } 289 /// A request to retry a wallet transaction by token. 290 fileprivate struct RetryProgressTokenNow: WalletBackendFormattedRequest { 291 struct Response: Decodable {} 292 var operation: String { "retryProgressTokenNow" } 293 func args() -> Args { Args(operation: op, progressToken: token) } 294 295 var op: String 296 var token: String 297 298 struct Args: Encodable { 299 var operation: String 300 var progressToken: String 301 } 302 } 303 // MARK: - 304 /// A request to get a wallet transaction by ID. 305 fileprivate struct GetTransactionById: WalletBackendFormattedRequest { 306 typealias Response = TalerTransaction 307 var operation: String { "getTransactionById" } 308 func args() -> Args { Args(transactionId: txId, includeContractTerms: inclTerms) } 309 310 var txId: String 311 var inclTerms: Bool? 312 313 struct Args: Encodable { 314 var transactionId: String 315 var includeContractTerms: Bool? 316 } 317 } 318 319 fileprivate struct JSONTransactionById: WalletBackendFormattedRequest { 320 typealias Response = String 321 var operation: String { "getTransactionById" } 322 func args() -> Args { Args(transactionId: transactionId, includeContractTerms: includeContractTerms) } 323 324 var transactionId: String 325 var includeContractTerms: Bool? 326 327 struct Args: Encodable { 328 var transactionId: String 329 var includeContractTerms: Bool? 330 } 331 } 332 333 extension WalletModel { 334 nonisolated func waitTransactionState(_ txId: String, token: String, 335 major: TransactionMajorState) 336 async throws -> WaitTransactionResult { 337 let txState = TransactionState(major: major, minor: .any, working: nil) 338 let request = WaitTransactionState(txId: txId, txState: txState, token: token) 339 let controller = Controller.shared 340 controller.progressOperation = request.operation 341 controller.progressToken = txId 342 controller.lastProgressError = nil 343 controller.lastProgressPhase = nil 344 let (response, id) = try await sendRequest(request) 345 return response 346 } 347 nonisolated func cancelProgressToken(_ op: String, token: String) 348 async throws { 349 let request = CancelProgressToken(op: op, token: token) 350 _ = try await sendRequest(request) 351 } 352 nonisolated func retryProgressTokenNow(_ op: String, token: String) 353 async throws { 354 let request = RetryProgressTokenNow(op: op, token: token) 355 _ = try await sendRequest(request) 356 } 357 /// get the specified transaction from Wallet-Core. No networking involved 358 nonisolated func getTransactionById(_ transactionId: String, includeContractTerms: Bool? = nil, viewHandles: Bool = false) 359 async throws -> TalerTransaction { 360 let request = GetTransactionById(txId: transactionId, inclTerms: includeContractTerms) 361 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 362 return response 363 } 364 nonisolated func jsonTransactionById(_ transactionId: String, includeContractTerms: Bool? = nil, viewHandles: Bool = false) 365 async throws -> String { 366 let request = JSONTransactionById(transactionId: transactionId, includeContractTerms: includeContractTerms) 367 let (response, id) = try await sendRequest(request, viewHandles: viewHandles, asJSON: true) 368 return response 369 } 370 } 371 // MARK: - 372 /// The info returned from Wallet-core init 373 struct VersionInfo: Decodable { 374 var implementationSemver: String? 375 var implementationGitHash: String? 376 var version: String 377 var exchange: String 378 var merchant: String 379 var bank: String 380 } 381 // MARK: - 382 fileprivate struct Testing: Encodable { 383 var denomselAllowLate: Bool 384 var devModeActive: Bool 385 var insecureTrustExchange: Bool 386 var preventThrottling: Bool 387 var skipDefaults: Bool 388 var emitObservabilityEvents: Bool 389 // more to come... 390 391 init(devModeActive: Bool) { 392 self.denomselAllowLate = false 393 self.devModeActive = devModeActive 394 self.insecureTrustExchange = false 395 self.preventThrottling = false 396 self.skipDefaults = false 397 self.emitObservabilityEvents = devModeActive 398 } 399 } 400 401 fileprivate struct Builtin: Encodable { 402 var exchanges: [String] 403 // more to come... 404 } 405 406 fileprivate struct Features: Encodable { 407 var migrateNativeDb: Bool // obsolete - use migrateDatabase() instead 408 var useNativeDb: Bool 409 } 410 411 fileprivate struct Config: Encodable { 412 var testing: Testing 413 var builtin: Builtin 414 var features: Features 415 } 416 // MARK: - 417 /// A request to re-configure Wallet-core 418 fileprivate struct ConfigRequest: WalletBackendFormattedRequest { 419 var setTesting: Bool 420 421 var operation: String { "setWalletRunConfig" } 422 func args() -> Args { 423 let testing = Testing(devModeActive: setTesting) 424 let builtin = Builtin(exchanges: []) 425 let features = Features(migrateNativeDb: false, useNativeDb: true) 426 let config = Config(testing: testing, builtin: builtin, features: features) 427 return Args(config: config) 428 } 429 430 struct Args: Encodable { 431 var config: Config 432 } 433 struct Response: Decodable { 434 var versionInfo: VersionInfo 435 } 436 } 437 438 extension WalletModel { 439 /// initalize Wallet-Core. Will do networking 440 @discardableResult 441 nonisolated func setConfig(setTesting: Bool) async throws -> VersionInfo { 442 let request = ConfigRequest(setTesting: setTesting) 443 let (response, id) = try await sendRequest(request) 444 return response.versionInfo 445 } 446 } 447 // MARK: - 448 struct InitResponse: Decodable { 449 var versionInfo: VersionInfo 450 var databaseBackend: String? 451 } 452 /// A request to initialize Wallet-core 453 fileprivate struct InitRequest: WalletBackendFormattedRequest { 454 var persistentPath: String 455 var temporaryPath: String? // folder 456 var setTesting: Bool 457 458 var operation: String { "init" } 459 func args() -> Args { 460 let testing = Testing(devModeActive: setTesting) 461 let builtin = Builtin(exchanges: []) 462 let features = Features(migrateNativeDb: false, useNativeDb: true) 463 let config = Config(testing: testing, builtin: builtin, features: features) 464 #if DEBUG 465 let logLevel = "info" // trace, info, message, warn, error, none 466 #else 467 let logLevel = "message" // less logging for AppStore builds 468 #endif 469 return Args(persistentStoragePath: persistentPath, // DB file 470 temporaryStoragePath: temporaryPath, 471 // cryptoWorkerType: "sync", qtart can ONLY use sync, that's the default anyway 472 logLevel: logLevel, 473 config: config, 474 useNativeLogging: true) 475 } 476 477 struct Args: Encodable { 478 var persistentStoragePath: String // DB file 479 var temporaryStoragePath: String? // folder 480 var logLevel: String 481 var config: Config 482 var useNativeLogging: Bool 483 } 484 typealias Response = InitResponse 485 } 486 487 extension WalletModel { 488 /// initalize Wallet-Core. Might do networking 489 nonisolated func initWalletCore(setTesting: Bool, viewHandles: Bool = false) async throws -> InitResponse { 490 let dbPath = try dbPath() 491 let tmpURL = URL.tempDirUrl 492 let request = InitRequest(persistentPath: dbPath, 493 temporaryPath: tmpURL?.path(withSlash: true), 494 setTesting: setTesting) 495 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 496 return response 497 } 498 499 private func dbUrl(_ folder: URL) -> URL { 500 let DATABASE = "talerwalletdb-v30" 501 let dbUrl = folder.appendingPathComponent(DATABASE, isDirectory: false) 502 .appendingPathExtension("sqlite3") 503 return dbUrl 504 } 505 506 private func checkAppSupport(_ url: URL) { 507 let fileManager = FileManager.default 508 var resultStorage: ObjCBool = false 509 510 if !fileManager.fileExists(atPath: url.path, isDirectory: &resultStorage) { 511 do { 512 try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 513 logger.debug("created \(url.path)") 514 } catch { 515 logger.error("creation failed \(error.localizedDescription)") 516 } 517 } else { 518 // logger.debug("\(url.path) exists") 519 } 520 } 521 522 private func migrate(from source: URL, to target: URL) { 523 let fileManager = FileManager.default 524 let sourceUrl = dbUrl(source) 525 let sourcePath = sourceUrl.path 526 let targetUrl = dbUrl(target) 527 let targetPath = targetUrl.path 528 529 checkAppSupport(target) 530 if fileManager.fileExists(atPath: sourcePath) { 531 do { 532 try fileManager.moveItem(at: sourceUrl, to: targetUrl) 533 logger.debug("migrate: moved to \(target.path)") 534 } catch { 535 logger.error("migrate: move failed \(error.localizedDescription)") 536 } 537 // } else { 538 // logger.debug("migrate: nothing to do, no db at \(sourcePath)") 539 } 540 541 // if fileManager.fileExists(atPath: targetPath) { 542 // logger.debug("found db at \(targetPath)") 543 // } else { 544 // logger.debug("migrate: nothing to do, no db at \(targetPath)") 545 // } 546 } 547 548 private func dbPath() throws -> String { 549 if let docDirUrl = URL.docDirUrl { 550 if let appSupport = URL.appSuppUrl { 551 #if DEBUG || GNU_TALER 552 migrate(from: appSupport, to: docDirUrl) 553 return docDirUrl.path(withSlash: true) 554 #else // TALER_WALLET or TALER_NIGHTLY 555 migrate(from: docDirUrl, to: appSupport) 556 return appSupport.path(withSlash: true) 557 #endif 558 } else { // should never happen 559 logger.error("dbPath: No applicationSupportDirectory") 560 } 561 } else { // should never happen 562 logger.error("dbPath: No documentDirectory") 563 } 564 throw WalletBackendError.initializationError 565 } 566 } 567 // MARK: - 568 /// A request to migrate the Wallet-core DB from indexed to native sqlite. 569 fileprivate struct MigrateRequest: WalletBackendFormattedRequest { 570 var operation: String { "migrateDatabase" } 571 func args() -> Args { Args(progressToken: operation) } 572 573 struct Args: Encodable { 574 var progressToken: String 575 } 576 typealias Response = MigrationResult // plus notifications 577 } 578 579 struct MigrationResult: Decodable { 580 var migrated: Bool? 581 var databaseBackend: String? 582 } 583 584 extension WalletModel { 585 /// reset Wallet-Core 586 nonisolated func migrateDatabase(viewHandles: Bool = false) 587 async throws -> MigrationResult { 588 let request = MigrateRequest() 589 let controller = Controller.shared 590 controller.progressOperation = request.operation 591 controller.progressToken = request.operation 592 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 593 return response 594 } 595 } 596 // MARK: - 597 /// A request to reset Wallet-core to a virgin DB. WILL DESTROY ALL COINS 598 fileprivate struct ResetRequest: WalletBackendFormattedRequest { 599 var operation: String { "clearDb" } 600 func args() -> Args { Args() } 601 602 struct Args: Encodable {} // no arguments needed 603 struct Response: Decodable {} 604 } 605 606 extension WalletModel { 607 /// reset Wallet-Core 608 nonisolated func resetWalletCore(viewHandles: Bool = false) async throws { 609 let request = ResetRequest() 610 _ = try await sendRequest(request, viewHandles: viewHandles) 611 } 612 } 613 // MARK: - 614 fileprivate struct ExportDbToFile: WalletBackendFormattedRequest { 615 var operation: String { "exportDbToFile" } 616 func args() -> Args { Args(directory: directory, stem: stem, forceFormat: asJSON ? "json" : nil) } 617 618 var directory: String 619 var stem: String 620 var asJSON: Bool 621 struct Args: Encodable { 622 var directory: String 623 var stem: String 624 var forceFormat: String? 625 } 626 struct Response: Decodable, Sendable { // path of the copied DB 627 var path: String 628 } 629 } 630 631 fileprivate struct ImportDbFromFile: WalletBackendFormattedRequest { 632 var operation: String { "importDbFromFile" } 633 func args() -> Args { Args(path: path ) } 634 635 var path: String 636 struct Args: Encodable { 637 var path: String 638 } 639 struct Response: Decodable {} 640 } 641 642 fileprivate struct GetDiagnostics: WalletBackendFormattedRequest { 643 var operation: String { "getDiagnostics" } 644 func args() -> Args { Args() } 645 struct Args: Encodable {} // no arguments needed 646 typealias Response = String 647 } 648 649 fileprivate struct GetPerformanceStats: WalletBackendFormattedRequest { 650 var operation: String { "testingGetPerformanceStats" } 651 func args() -> Args { Args() } 652 struct Args: Encodable {} // no arguments needed 653 typealias Response = String 654 } 655 656 extension WalletModel { 657 /// export, import DB, get diagnostics 658 nonisolated func exportDbToFile(stem: String, asJSON: Bool = true, viewHandles: Bool = false) 659 async throws -> String? { 660 if let docDirUrl = URL.docDirUrl { 661 let dbPath = docDirUrl.path(withSlash: false) 662 let request = ExportDbToFile(directory: dbPath, stem: stem, asJSON: asJSON) 663 print(dbPath, stem) 664 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 665 return response.path 666 } else { 667 return nil 668 } 669 } 670 nonisolated func importDbFromFile(path: String, viewHandles: Bool = false) 671 async throws { 672 let request = ImportDbFromFile(path: path) 673 _ = try await sendRequest(request, viewHandles: viewHandles) 674 } 675 nonisolated func getDiagnostics(viewHandles: Bool = false) 676 async throws -> String { 677 let request = GetDiagnostics() 678 let (response, id) = try await sendRequest(request, viewHandles: viewHandles, asJSON: true) 679 return response 680 } 681 nonisolated func getPerformanceStats(viewHandles: Bool = false) 682 async throws -> String { 683 let request = GetPerformanceStats() 684 let (response, id) = try await sendRequest(request, viewHandles: viewHandles, asJSON: true) 685 return response 686 } 687 } 688 // MARK: - 689 fileprivate struct DevExperimentRequest: WalletBackendFormattedRequest { 690 var operation: String { "applyDevExperiment" } 691 func args() -> Args { Args(devExperimentUri: talerUri) } 692 693 var talerUri: String 694 695 struct Args: Encodable { 696 var devExperimentUri: String 697 } 698 struct Response: Decodable {} 699 } 700 701 extension WalletModel { 702 /// tell wallet-core to mock new transactions 703 nonisolated func devExperimentT(_ talerUri: String, viewHandles: Bool = false) async throws { 704 // T for any Thread 705 let request = DevExperimentRequest(talerUri: talerUri) 706 _ = try await sendRequest(request, viewHandles: viewHandles) 707 } 708 }