// // TestCoordinator.swift // ZcashLightClientKit-Unit-Tests // // Created by Francisco Gindre on 4/29/20. // import Foundation @testable import ZcashLightClientKit /** This is the TestCoordinator What does it do? quite a lot. Is it a nice "SOLID" "Clean Code" piece of source code? Hell no. It's your testing overlord and you will be grateful it is. */ // swiftlint:disable force_try function_parameter_count class TestCoordinator { enum CoordinatorError: Error { case notDarksideWallet case notificationFromUnknownSynchronizer case notMockLightWalletService case builderError case seedRequiredForMigration } enum SyncThreshold { case upTo(height: BlockHeight) case latestHeight } enum DarksideData { case `default` case predefined(dataset: DarksideDataset) case url(urlString: String, startHeigth: BlockHeight) } var completionHandler: ((SDKSynchronizer) throws -> Void)? var errorHandler: ((Error?) -> Void)? var spendingKey: UnifiedSpendingKey var birthday: BlockHeight var channelProvider: ChannelProvider var synchronizer: SDKSynchronizer var service: DarksideWalletService var spendingKeys: [UnifiedSpendingKey]? var databases: TemporaryTestDatabases let network: ZcashNetwork convenience init( seed: String, walletBirthday: BlockHeight, channelProvider: ChannelProvider, network: ZcashNetwork ) async throws { let derivationTool = DerivationTool(networkType: network.networkType) let spendingKey = try derivationTool.deriveUnifiedSpendingKey( seed: TestSeed().seed(), accountIndex: 0 ) let ufvk = try derivationTool.deriveUnifiedFullViewingKey(from: spendingKey) await try self.init( spendingKey: spendingKey, unifiedFullViewingKey: ufvk, walletBirthday: walletBirthday, channelProvider: channelProvider, network: network ) } required init( spendingKey: UnifiedSpendingKey, unifiedFullViewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight, channelProvider: ChannelProvider, network: ZcashNetwork ) async throws { self.spendingKey = spendingKey self.birthday = walletBirthday self.channelProvider = channelProvider self.databases = TemporaryDbBuilder.build() self.network = network self.service = DarksideWalletService( service: LightWalletGRPCService( host: Constants.address, port: 9067, secure: false, singleCallTimeout: 10000, streamingCallTimeout: 1000000 ) ) let storage = CompactBlockStorage(url: databases.cacheDB, readonly: false) try storage.createTable() let buildResult = try await TestSynchronizerBuilder.build( rustBackend: ZcashRustBackend.self, lowerBoundHeight: self.birthday, cacheDbURL: databases.cacheDB, dataDbURL: databases.dataDB, pendingDbURL: databases.pendingDB, endpoint: LightWalletEndpointBuilder.default, service: self.service, repository: TransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: databases.dataDB.absoluteString)), accountRepository: AccountRepositoryBuilder.build( dataDbURL: databases.dataDB, readOnly: true ), storage: storage, spendParamsURL: try __spendParamsURL(), outputParamsURL: try __outputParamsURL(), spendingKey: spendingKey, unifiedFullViewingKey: unifiedFullViewingKey, walletBirthday: walletBirthday, network: network, loggerProxy: SampleLogger(logLevel: .debug) ) self.synchronizer = buildResult.synchronizer self.spendingKeys = buildResult.spendingKeys subscribeToNotifications(synchronizer: self.synchronizer) } func stop() throws { synchronizer.stop() self.completionHandler = nil self.errorHandler = nil } func setDarksideWalletState(_ state: DarksideData) throws { switch state { case .default: try service.useDataset(DarksideDataset.beforeReOrg.rawValue) case .predefined(let dataset): try service.useDataset(dataset.rawValue) case .url(let urlString, _): try service.useDataset(from: urlString) } } func setLatestHeight(height: BlockHeight) throws { try service.applyStaged(nextLatestHeight: height) } func sync(completion: @escaping (SDKSynchronizer) throws -> Void, error: @escaping (Error?) -> Void) throws { self.completionHandler = completion self.errorHandler = error try synchronizer.start(retry: true) } /** Notifications */ func subscribeToNotifications(synchronizer: Synchronizer) { NotificationCenter.default.addObserver(self, selector: #selector(synchronizerFailed(_:)), name: .synchronizerFailed, object: synchronizer) NotificationCenter.default.addObserver(self, selector: #selector(synchronizerSynced(_:)), name: .synchronizerSynced, object: synchronizer) } @objc func synchronizerFailed(_ notification: Notification) { self.errorHandler?(notification.userInfo?[SDKSynchronizer.NotificationKeys.error] as? Error) } @objc func synchronizerSynced(_ notification: Notification) throws { if case .stopped = self.synchronizer.status { LoggerProxy.debug("WARNING: notification received after synchronizer was stopped") return } try self.completionHandler?(self.synchronizer) } @objc func synchronizerDisconnected(_ notification: Notification) { /// TODO: See if we need hooks for this } @objc func synchronizerStarted(_ notification: Notification) { /// TODO: See if we need hooks for this } @objc func synchronizerStopped(_ notification: Notification) { /// TODO: See if we need hooks for this } @objc func synchronizerSyncing(_ notification: Notification) { /// TODO: See if we need hooks for this } } extension CompactBlockProcessor { public func setConfig(_ config: Configuration) { self.config = config } } extension TestCoordinator { func resetBlocks(dataset: DarksideData) throws { switch dataset { case .default: try service.useDataset(DarksideDataset.beforeReOrg.rawValue) case .predefined(let blocks): try service.useDataset(blocks.rawValue) case .url(let urlString, _): try service.useDataset(urlString) } } func stageBlockCreate(height: BlockHeight, count: Int = 1, nonce: Int = 0) throws { try service.stageBlocksCreate(from: height, count: count, nonce: 0) } func applyStaged(blockheight: BlockHeight) throws { try service.applyStaged(nextLatestHeight: blockheight) } func stageTransaction(_ transaction: RawTransaction, at height: BlockHeight) throws { try service.stageTransaction(transaction, at: height) } func stageTransaction(url: String, at height: BlockHeight) throws { try service.stageTransaction(from: url, at: height) } func latestHeight() throws -> BlockHeight { try service.latestBlockHeight() } func reset(saplingActivation: BlockHeight, branchID: String, chainName: String) throws { Task { await self.synchronizer.blockProcessor.stop() let config = await self.synchronizer.blockProcessor.config let newConfig = CompactBlockProcessor.Configuration( cacheDb: config.cacheDb, dataDb: config.dataDb, downloadBatchSize: config.downloadBatchSize, retries: config.retries, maxBackoffInterval: config.maxBackoffInterval, rewindDistance: config.rewindDistance, walletBirthday: config.walletBirthday, saplingActivation: config.saplingActivation, network: config.network ) await self.synchronizer.blockProcessor.setConfig(newConfig) } try service.reset(saplingActivation: saplingActivation, branchID: branchID, chainName: chainName) } func getIncomingTransactions() throws -> [RawTransaction]? { return try service.getIncomingTransactions() } } struct TemporaryTestDatabases { var cacheDB: URL var dataDB: URL var pendingDB: URL } enum TemporaryDbBuilder { static func build() -> TemporaryTestDatabases { let tempUrl = try! __documentsDirectory() let timestamp = String(Int(Date().timeIntervalSince1970)) return TemporaryTestDatabases( cacheDB: tempUrl.appendingPathComponent("cache_db_\(timestamp).db"), dataDB: tempUrl.appendingPathComponent("data_db_\(timestamp).db"), pendingDB: tempUrl.appendingPathComponent("pending_db_\(timestamp).db") ) } } enum TestSynchronizerBuilder { static func build( rustBackend: ZcashRustBackendWelding.Type, lowerBoundHeight: BlockHeight, cacheDbURL: URL, dataDbURL: URL, pendingDbURL: URL, endpoint: LightWalletEndpoint, service: LightWalletService, repository: TransactionRepository, accountRepository: AccountRepository, storage: CompactBlockStorage, spendParamsURL: URL, outputParamsURL: URL, spendingKey: UnifiedSpendingKey, unifiedFullViewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight, network: ZcashNetwork, seed: [UInt8]? = nil, loggerProxy: Logger? = nil ) async throws -> (spendingKeys: [UnifiedSpendingKey]?, synchronizer: SDKSynchronizer) { let initializer = Initializer( cacheDbURL: cacheDbURL, dataDbURL: dataDbURL, pendingDbURL: pendingDbURL, endpoint: endpoint, network: network, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL, viewingKeys: [unifiedFullViewingKey], walletBirthday: walletBirthday, alias: "", loggerProxy: loggerProxy ) let synchronizer = try SDKSynchronizer(initializer: initializer) if case .seedRequired = try await synchronizer.prepare(with: seed) { throw TestCoordinator.CoordinatorError.seedRequiredForMigration } return ([spendingKey], synchronizer) } static func build( rustBackend: ZcashRustBackendWelding.Type, lowerBoundHeight: BlockHeight, cacheDbURL: URL, dataDbURL: URL, pendingDbURL: URL, endpoint: LightWalletEndpoint, service: LightWalletService, repository: TransactionRepository, accountRepository: AccountRepository, storage: CompactBlockStorage, spendParamsURL: URL, outputParamsURL: URL, seedBytes: [UInt8], walletBirthday: BlockHeight, network: ZcashNetwork, loggerProxy: Logger? = nil ) async throws -> (spendingKeys: [UnifiedSpendingKey]?, synchronizer: SDKSynchronizer) { let spendingKey = try DerivationTool(networkType: network.networkType) .deriveUnifiedSpendingKey(seed: seedBytes, accountIndex: 0) let uvk = try DerivationTool(networkType: network.networkType) .deriveUnifiedFullViewingKey(from: spendingKey) return try await build( rustBackend: rustBackend, lowerBoundHeight: lowerBoundHeight, cacheDbURL: cacheDbURL, dataDbURL: dataDbURL, pendingDbURL: pendingDbURL, endpoint: endpoint, service: service, repository: repository, accountRepository: accountRepository, storage: storage, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL, spendingKey: spendingKey, unifiedFullViewingKey: uvk, walletBirthday: walletBirthday, network: network ) } }