diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift index c99963b9..dd23da60 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift @@ -150,7 +150,6 @@ class SyncBlocksViewController: UIViewController { case .unprepared, .error: do { if syncStatus == .unprepared { - // swiftlint:disable:next force_try do { _ = try await synchronizer.prepare( with: DemoAppConfig.defaultSeed, diff --git a/Sources/ZcashLightClientKit/Block/Actions/Action.swift b/Sources/ZcashLightClientKit/Block/Actions/Action.swift new file mode 100644 index 00000000..05312573 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/Action.swift @@ -0,0 +1,61 @@ +// +// Action.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +actor ActionContext { + var state: CBPState + var syncRanges: SyncRanges + var totalProgressRange: CompactBlockRange = 0...0 + + init(state: CBPState) { + self.state = state + syncRanges = SyncRanges.empty + } + + func update(state: CBPState) async { self.state = state } + func update(syncRanges: SyncRanges) async { self.syncRanges = syncRanges } + func update(totalProgressRange: CompactBlockRange) async { self.totalProgressRange = totalProgressRange } +} + +enum CBPState: CaseIterable { + case idle + case migrateLegacyCacheDB + case validateServer + case computeSyncRanges + case checksBeforeSync + case download + case validate + case scan + case clearAlreadyScannedBlocks + case enhance + case fetchUTXO + case handleSaplingParams + case clearCache + case finished + case failed + case stopped +} + +protocol Action { + /// If this is true and action fails with error then blocks cache is cleared. + var removeBlocksCacheWhenFailed: Bool { get } + + // When any action is created it can get `DIContainer` and resolve any depedencies it requires. + // Every action uses `context` to get some informartion like download range. + // + // `didUpdate` is closure that action use to tell CBP that some part of the work is done. For example if download action would like to + // update progress on every block downloaded it can use this closure. Also if action doesn't need to update progress on partial work it doesn't + // need to use this closure at all. + // + // Each action updates context accordingly. It should at least set new state. Reason for this is that action can return different states for + // different conditions. And action is the thing that knows these conditions. + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext + + // Should be called on each existing action when processor wants to stop. Some actions may do it's own background work. + func stop() async +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ChecksBeforeSyncAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ChecksBeforeSyncAction.swift new file mode 100644 index 00000000..a49d5133 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ChecksBeforeSyncAction.swift @@ -0,0 +1,62 @@ +// +// ChecksBeforeSyncAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ChecksBeforeSyncAction { + let internalSyncProgress: InternalSyncProgress + let storage: CompactBlockRepository + init(container: DIContainer) { + internalSyncProgress = container.resolve(InternalSyncProgress.self) + storage = container.resolve(CompactBlockRepository.self) + } + + /// Tells whether the state represented by these sync ranges evidence some sort of + /// outdated state on the cache or the internal state of the compact block processor. + /// + /// - Note: this can mean that the processor has synced over the height that the internal + /// state knows of because the sync process was interrupted before it could reflect + /// it in the internal state storage. This could happen because of many factors, the + /// most feasible being OS shutting down a background process or the user abruptly + /// exiting the app. + /// - Returns: an ``Optional`` where Some represents what's the + /// new state the internal state should reflect and indicating that the cache should be cleared + /// as well. `nil` means that no action is required. + func shouldClearBlockCacheAndUpdateInternalState(syncRange: SyncRanges) -> BlockHeight? { + guard syncRange.downloadRange != nil, syncRange.scanRange != nil else { return nil } + + guard + let latestScannedHeight = syncRange.latestScannedHeight, + let latestDownloadedHeight = syncRange.latestDownloadedBlockHeight, + latestScannedHeight > latestDownloadedHeight + else { return nil } + + return latestScannedHeight + } +} + +extension ChecksBeforeSyncAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + // clear any present cached state if needed. + // this checks if there was a sync in progress that was + // interrupted abruptly and cache was not able to be cleared + // properly and internal state set to the appropriate value + if let newLatestDownloadedHeight = shouldClearBlockCacheAndUpdateInternalState(syncRange: await context.syncRanges) { + try await storage.clear() + try await internalSyncProgress.set(newLatestDownloadedHeight, .latestDownloadedBlockHeight) + } else { + try await storage.create() + } + + await context.update(state: .fetchUTXO) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift new file mode 100644 index 00000000..e821ace8 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift @@ -0,0 +1,31 @@ +// +// ClearCacheForLastScannedBatch.swift +// +// +// Created by Michal Fousek on 08.05.2023. +// + +import Foundation + +final class ClearAlreadyScannedBlocksAction { + let storage: CompactBlockRepository + let transactionRepository: TransactionRepository + init(container: DIContainer) { + storage = container.resolve(CompactBlockRepository.self) + transactionRepository = container.resolve(TransactionRepository.self) + } +} + +extension ClearAlreadyScannedBlocksAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + let lastScannedHeight = try await transactionRepository.lastScannedHeight() + try await storage.clear(upTo: lastScannedHeight) + + await context.update(state: .enhance) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift new file mode 100644 index 00000000..0f99a0ce --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift @@ -0,0 +1,27 @@ +// +// ClearCacheAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ClearCacheAction { + let storage: CompactBlockRepository + init(container: DIContainer) { + storage = container.resolve(CompactBlockRepository.self) + } +} + +extension ClearCacheAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + try await storage.clear() + await context.update(state: .finished) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ComputeSyncRangesAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ComputeSyncRangesAction.swift new file mode 100644 index 00000000..e5a6f6eb --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ComputeSyncRangesAction.swift @@ -0,0 +1,93 @@ +// +// ComputeSyncRangesAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ComputeSyncRangesAction { + let configProvider: CompactBlockProcessor.ConfigProvider + let downloaderService: BlockDownloaderService + let internalSyncProgress: InternalSyncProgress + let latestBlocksDataProvider: LatestBlocksDataProvider + let logger: Logger + + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + self.configProvider = configProvider + downloaderService = container.resolve(BlockDownloaderService.self) + internalSyncProgress = container.resolve(InternalSyncProgress.self) + latestBlocksDataProvider = container.resolve(LatestBlocksDataProvider.self) + logger = container.resolve(Logger.self) + } + + /// This method analyses what must be done and computes range that should be used to compute reported progress. + func computeTotalProgressRange(from syncRanges: SyncRanges) -> CompactBlockRange { + guard syncRanges.downloadRange != nil || syncRanges.scanRange != nil else { + // In this case we are sure that no downloading or scanning happens so this returned range won't be even used. And it's easier to return + // this "fake" range than to handle nil. + return 0...0 + } + + // Thanks to guard above we can be sure that one of these two ranges is not nil. + let lowerBound = syncRanges.scanRange?.lowerBound ?? syncRanges.downloadRange?.lowerBound ?? 0 + let upperBound = syncRanges.scanRange?.upperBound ?? syncRanges.downloadRange?.upperBound ?? 0 + + return lowerBound...upperBound + } +} + +extension ComputeSyncRangesAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + // call internalSyncProgress and compute sync ranges and store them in context + // if there is nothing sync just switch to finished state + + let config = await configProvider.config + let latestDownloadHeight = try await downloaderService.lastDownloadedBlockHeight() + + try await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadHeight, alias: config.alias) + + await latestBlocksDataProvider.updateScannedData() + await latestBlocksDataProvider.updateBlockData() + + let nextState = try await internalSyncProgress.computeNextState( + latestBlockHeight: latestBlocksDataProvider.latestBlockHeight, + latestScannedHeight: latestBlocksDataProvider.latestScannedHeight, + walletBirthday: config.walletBirthday + ) + + switch nextState { + case .finishProcessing: + await context.update(state: .finished) + case .processNewBlocks(let ranges): + let totalProgressRange = computeTotalProgressRange(from: ranges) + await context.update(totalProgressRange: totalProgressRange) + await context.update(syncRanges: ranges) + await context.update(state: .checksBeforeSync) + + logger.debug(""" + Syncing with ranges: + download: \(ranges.downloadRange?.lowerBound ?? -1)...\(ranges.downloadRange?.upperBound ?? -1) + scan: \(ranges.scanRange?.lowerBound ?? -1)...\(ranges.scanRange?.upperBound ?? -1) + enhance range: \(ranges.enhanceRange?.lowerBound ?? -1)...\(ranges.enhanceRange?.upperBound ?? -1) + fetchUTXO range: \(ranges.fetchUTXORange?.lowerBound ?? -1)...\(ranges.fetchUTXORange?.upperBound ?? -1) + total progress range: \(totalProgressRange.lowerBound)...\(totalProgressRange.upperBound) + """) + + case let .wait(latestHeight, latestDownloadHeight): + // Lightwalletd might be syncing + logger.info( + "Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight) " + + "while latest blockheight is reported at: \(latestHeight)" + ) + await context.update(state: .finished) + } + + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift new file mode 100644 index 00000000..2ed1cb47 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift @@ -0,0 +1,64 @@ +// +// DownloadAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class DownloadAction { + let configProvider: CompactBlockProcessor.ConfigProvider + let downloader: BlockDownloader + let transactionRepository: TransactionRepository + let logger: Logger + + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + self.configProvider = configProvider + downloader = container.resolve(BlockDownloader.self) + transactionRepository = container.resolve(TransactionRepository.self) + logger = container.resolve(Logger.self) + } + + private func update(context: ActionContext) async -> ActionContext { + await context.update(state: .validate) + return context + } +} + +extension DownloadAction: Action { + var removeBlocksCacheWhenFailed: Bool { true } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + guard let downloadRange = await context.syncRanges.downloadRange else { + return await update(context: context) + } + + let config = await configProvider.config + let lastScannedHeight = try await transactionRepository.lastScannedHeight() + // This action is executed for each batch (batch size is 100 blocks by default) until all the blocks in whole `downloadRange` are downloaded. + // So the right range for this batch must be computed. + let batchRangeStart = max(downloadRange.lowerBound, lastScannedHeight) + let batchRangeEnd = min(downloadRange.upperBound, batchRangeStart + config.batchSize) + + guard batchRangeStart <= batchRangeEnd else { + return await update(context: context) + } + + let batchRange = batchRangeStart...batchRangeEnd + let downloadLimit = batchRange.upperBound + (2 * config.batchSize) + + logger.debug("Starting download with range: \(batchRange.lowerBound)...\(batchRange.upperBound)") + try await downloader.setSyncRange(downloadRange, batchSize: config.batchSize) + await downloader.setDownloadLimit(downloadLimit) + await downloader.startDownload(maxBlockBufferSize: config.downloadBufferSize) + + try await downloader.waitUntilRequestedBlocksAreDownloaded(in: batchRange) + + return await update(context: context) + } + + func stop() async { + await downloader.stopDownload() + } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift new file mode 100644 index 00000000..669b1c2a --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift @@ -0,0 +1,91 @@ +// +// EnhanceAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class EnhanceAction { + let blockEnhancer: BlockEnhancer + let configProvider: CompactBlockProcessor.ConfigProvider + let internalSyncProgress: InternalSyncProgress + let logger: Logger + let transactionRepository: TransactionRepository + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + blockEnhancer = container.resolve(BlockEnhancer.self) + self.configProvider = configProvider + internalSyncProgress = container.resolve(InternalSyncProgress.self) + logger = container.resolve(Logger.self) + transactionRepository = container.resolve(TransactionRepository.self) + } + + func decideWhatToDoNext(context: ActionContext, lastScannedHeight: BlockHeight) async -> ActionContext { + guard let scanRange = await context.syncRanges.scanRange else { + await context.update(state: .clearCache) + return context + } + + if lastScannedHeight >= scanRange.upperBound { + await context.update(state: .clearCache) + } else { + await context.update(state: .download) + } + + return context + } +} + +extension EnhanceAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + // Use `BlockEnhancer` to enhance blocks. + // This action is executed on each downloaded and scanned batch (typically each 100 blocks). But we want to run enhancement each 1000 blocks. + // This action can use `InternalSyncProgress` and last scanned height to compute when it should do work. + + // if latestScannedHeight >= context.scanRanges.scanRange.upperBound then everything is processed and sync process should continue to end. + // If latestScannedHeight < context.scanRanges.scanRange.upperBound then set state to `download` because there are blocks to + // download and scan. + + let config = await configProvider.config + let lastScannedHeight = try await transactionRepository.lastScannedHeight() + + guard let range = await context.syncRanges.enhanceRange else { + return await decideWhatToDoNext(context: context, lastScannedHeight: lastScannedHeight) + } + + let lastEnhancedHeight = try await internalSyncProgress.load(.latestEnhancedHeight) + let enhanceRangeStart = max(range.lowerBound, lastEnhancedHeight) + let enhanceRangeEnd = min(range.upperBound, lastScannedHeight) + + // This may happen: + // For example whole enhance range is 0...2100 Without this force enhance is done for ranges: 0...1000, 1001...2000. And that's it. + // Last 100 blocks isn't enhanced. + // + // This force makes sure that all the blocks are enhanced even when last enhance happened < 1000 blocks ago. + let forceEnhance = enhanceRangeEnd == range.upperBound && enhanceRangeEnd - enhanceRangeStart <= config.enhanceBatchSize + + if forceEnhance || (enhanceRangeStart <= enhanceRangeEnd && lastScannedHeight - lastEnhancedHeight >= config.enhanceBatchSize) { + let enhanceRange = enhanceRangeStart...enhanceRangeEnd + let transactions = try await blockEnhancer.enhance( + at: enhanceRange, + didEnhance: { progress in + if let foundTx = progress.lastFoundTransaction, progress.newlyMined { + await didUpdate(.minedTransaction(foundTx)) + await didUpdate(.progressPartialUpdate(.enhance(progress))) + } + } + ) + + if let transactions { + await didUpdate(.foundTransactions(transactions, enhanceRange)) + } + } + + return await decideWhatToDoNext(context: context, lastScannedHeight: lastScannedHeight) + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/FetchUTXOsAction.swift b/Sources/ZcashLightClientKit/Block/Actions/FetchUTXOsAction.swift new file mode 100644 index 00000000..ba7e42f0 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/FetchUTXOsAction.swift @@ -0,0 +1,37 @@ +// +// FetchUTXOsAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class FetchUTXOsAction { + let utxoFetcher: UTXOFetcher + let logger: Logger + + init(container: DIContainer) { + utxoFetcher = container.resolve(UTXOFetcher.self) + logger = container.resolve(Logger.self) + } +} + +extension FetchUTXOsAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + if let range = await context.syncRanges.fetchUTXORange { + logger.debug("Fetching UTXO with range: \(range.lowerBound)...\(range.upperBound)") + let result = try await utxoFetcher.fetch(at: range) { fetchProgress in + await didUpdate(.progressPartialUpdate(.fetch(fetchProgress))) + } + await didUpdate(.storedUTXOs(result)) + } + + await context.update(state: .handleSaplingParams) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/MigrateLegacyCacheDBAction.swift b/Sources/ZcashLightClientKit/Block/Actions/MigrateLegacyCacheDBAction.swift new file mode 100644 index 00000000..5cb70c03 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/MigrateLegacyCacheDBAction.swift @@ -0,0 +1,79 @@ +// +// MigrateLegacyCacheDB.swift +// +// +// Created by Michal Fousek on 10.05.2023. +// + +import Foundation + +final class MigrateLegacyCacheDBAction { + private let configProvider: CompactBlockProcessor.ConfigProvider + private let internalSyncProgress: InternalSyncProgress + private let storage: CompactBlockRepository + private let transactionRepository: TransactionRepository + private let fileManager: ZcashFileManager + + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + self.configProvider = configProvider + internalSyncProgress = container.resolve(InternalSyncProgress.self) + storage = container.resolve(CompactBlockRepository.self) + transactionRepository = container.resolve(TransactionRepository.self) + fileManager = container.resolve(ZcashFileManager.self) + } + + private func updateState(_ context: ActionContext) async -> ActionContext { + await context.update(state: .validateServer) + return context + } +} + +extension MigrateLegacyCacheDBAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + let config = await configProvider.config + guard let legacyCacheDbURL = config.cacheDbURL else { + return await updateState(context) + } + + guard legacyCacheDbURL != config.fsBlockCacheRoot else { + throw ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL + } + + // Instance with alias `default` is same as instance before the Alias was introduced. So it makes sense that only this instance handles + // legacy cache DB. Any instance with different than `default` alias was created after the Alias was introduced and at this point legacy + // cache DB is't anymore. So there is nothing to migrate for instances with not default Alias. + guard config.alias == .default else { + return await updateState(context) + } + + // if the URL provided is not readable, it means that the client has a reference + // to the cacheDb file but it has been deleted in a prior sync cycle. there's + // nothing to do here. + guard fileManager.isReadableFile(atPath: legacyCacheDbURL.path) else { + return await updateState(context) + } + + do { + // if there's a readable file at the provided URL, delete it. + try fileManager.removeItem(at: legacyCacheDbURL) + } catch { + throw ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb(error) + } + + // create the storage + try await self.storage.create() + + // The database has been deleted, so we have adjust the internal state of the + // `CompactBlockProcessor` so that it doesn't rely on download heights set + // by a previous processing cycle. + let lastScannedHeight = try await transactionRepository.lastScannedHeight() + + try await internalSyncProgress.set(lastScannedHeight, .latestDownloadedBlockHeight) + + return await updateState(context) + } + + func stop() { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift b/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift new file mode 100644 index 00000000..0ba25917 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift @@ -0,0 +1,31 @@ +// +// SaplingParamsAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class SaplingParamsAction { + let saplingParametersHandler: SaplingParametersHandler + let logger: Logger + + init(container: DIContainer) { + saplingParametersHandler = container.resolve(SaplingParametersHandler.self) + logger = container.resolve(Logger.self) + } +} + +extension SaplingParamsAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + logger.debug("Fetching sapling parameters") + try await saplingParametersHandler.handleIfNeeded() + await context.update(state: .download) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift new file mode 100644 index 00000000..2e5a6c4a --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift @@ -0,0 +1,66 @@ +// +// ScanAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ScanAction { + let configProvider: CompactBlockProcessor.ConfigProvider + let blockScanner: BlockScanner + let logger: Logger + let transactionRepository: TransactionRepository + + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + self.configProvider = configProvider + blockScanner = container.resolve(BlockScanner.self) + transactionRepository = container.resolve(TransactionRepository.self) + logger = container.resolve(Logger.self) + } + + private func update(context: ActionContext) async -> ActionContext { + await context.update(state: .clearAlreadyScannedBlocks) + return context + } +} + +extension ScanAction: Action { + var removeBlocksCacheWhenFailed: Bool { true } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + guard let scanRange = await context.syncRanges.scanRange else { + return await update(context: context) + } + + let config = await configProvider.config + let lastScannedHeight = try await transactionRepository.lastScannedHeight() + // This action is executed for each batch (batch size is 100 blocks by default) until all the blocks in whole `scanRange` are scanned. + // So the right range for this batch must be computed. + let batchRangeStart = max(scanRange.lowerBound, lastScannedHeight) + let batchRangeEnd = min(scanRange.upperBound, batchRangeStart + config.batchSize) + + guard batchRangeStart <= batchRangeEnd else { + return await update(context: context) + } + + let batchRange = batchRangeStart...batchRangeStart + config.batchSize + + logger.debug("Starting scan blocks with range: \(batchRange.lowerBound)...\(batchRange.upperBound)") + let totalProgressRange = await context.totalProgressRange + try await blockScanner.scanBlocks(at: batchRange, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in + let progress = BlockProgress( + startHeight: totalProgressRange.lowerBound, + targetHeight: totalProgressRange.upperBound, + progressHeight: lastScannedHeight + ) + self?.logger.debug("progress: \(progress)") + await didUpdate(.progressPartialUpdate(.syncing(progress))) + } + + return await update(context: context) + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ValidateAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ValidateAction.swift new file mode 100644 index 00000000..7af1f343 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ValidateAction.swift @@ -0,0 +1,28 @@ +// +// ValidateAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ValidateAction { + let validator: BlockValidator + + init(container: DIContainer) { + validator = container.resolve(BlockValidator.self) + } +} + +extension ValidateAction: Action { + var removeBlocksCacheWhenFailed: Bool { true } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + try await validator.validate() + await context.update(state: .scan) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/ValidateServerAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ValidateServerAction.swift new file mode 100644 index 00000000..0cf0ed55 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ValidateServerAction.swift @@ -0,0 +1,60 @@ +// +// ValidateServerAction.swift +// +// +// Created by Michal Fousek on 05.05.2023. +// + +import Foundation + +final class ValidateServerAction { + let configProvider: CompactBlockProcessor.ConfigProvider + let rustBackend: ZcashRustBackendWelding + let service: LightWalletService + + init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { + self.configProvider = configProvider + rustBackend = container.resolve(ZcashRustBackendWelding.self) + service = container.resolve(LightWalletService.self) + } +} + +extension ValidateServerAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + let config = await configProvider.config + let info = try await service.getInfo() + let localNetwork = config.network + let saplingActivation = config.saplingActivation + + // check network types + guard let remoteNetworkType = NetworkType.forChainName(info.chainName) else { + throw ZcashError.compactBlockProcessorChainName(info.chainName) + } + + guard remoteNetworkType == localNetwork.networkType else { + throw ZcashError.compactBlockProcessorNetworkMismatch(localNetwork.networkType, remoteNetworkType) + } + + guard saplingActivation == info.saplingActivationHeight else { + throw ZcashError.compactBlockProcessorSaplingActivationMismatch(saplingActivation, BlockHeight(info.saplingActivationHeight)) + } + + // check branch id + let localBranch = try rustBackend.consensusBranchIdFor(height: Int32(info.blockHeight)) + + guard let remoteBranchID = ConsensusBranchID.fromString(info.consensusBranchID) else { + throw ZcashError.compactBlockProcessorConsensusBranchID + } + + guard remoteBranchID == localBranch else { + throw ZcashError.compactBlockProcessorWrongConsensusBranchId(localBranch, remoteBranchID) + } + + await context.update(state: .computeSyncRanges) + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index 67dd709d..c052b24d 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -5,144 +5,47 @@ // Created by Francisco Gindre on 18/09/2019. // Copyright © 2019 Electric Coin Company. All rights reserved. // -// swiftlint:disable file_length type_body_length import Foundation import Combine public typealias RefreshedUTXOs = (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]) -public enum CompactBlockProgress { - case syncing(_ progress: BlockProgress) - case enhance(_ progress: EnhancementProgress) - case fetch(_ progress: Float) - - public var progress: Float { - switch self { - case .syncing(let blockProgress): - return blockProgress.progress - case .enhance(let enhancementProgress): - return enhancementProgress.progress - case .fetch(let fetchingProgress): - return fetchingProgress - } - } - - public var progressHeight: BlockHeight? { - switch self { - case .syncing(let blockProgress): - return blockProgress.progressHeight - case .enhance(let enhancementProgress): - return enhancementProgress.lastFoundTransaction?.minedHeight - default: - return 0 - } - } - - public var blockDate: Date? { - if case .enhance(let enhancementProgress) = self, let time = enhancementProgress.lastFoundTransaction?.blockTime { - return Date(timeIntervalSince1970: time) - } - - return nil - } - - public var targetHeight: BlockHeight? { - switch self { - case .syncing(let blockProgress): - return blockProgress.targetHeight - default: - return nil - } - } -} - -public struct EnhancementProgress: Equatable { - /// total transactions that were detected in the `range` - public let totalTransactions: Int - /// enhanced transactions so far - public let enhancedTransactions: Int - /// last found transaction - public let lastFoundTransaction: ZcashTransaction.Overview? - /// block range that's being enhanced - public let range: CompactBlockRange - /// whether this transaction can be considered `newly mined` and not part of the - /// wallet catching up to stale and uneventful blocks. - public let newlyMined: Bool - - public init( - totalTransactions: Int, - enhancedTransactions: Int, - lastFoundTransaction: ZcashTransaction.Overview?, - range: CompactBlockRange, - newlyMined: Bool - ) { - self.totalTransactions = totalTransactions - self.enhancedTransactions = enhancedTransactions - self.lastFoundTransaction = lastFoundTransaction - self.range = range - self.newlyMined = newlyMined - } - - public var progress: Float { - totalTransactions > 0 ? Float(enhancedTransactions) / Float(totalTransactions) : 0 - } - - public static var zero: EnhancementProgress { - EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false) - } - - public static func == (lhs: EnhancementProgress, rhs: EnhancementProgress) -> Bool { - return - lhs.totalTransactions == rhs.totalTransactions && - lhs.enhancedTransactions == rhs.enhancedTransactions && - lhs.lastFoundTransaction?.id == rhs.lastFoundTransaction?.id && - lhs.range == rhs.range - } -} - /// The compact block processor is in charge of orchestrating the download and caching of compact blocks from a LightWalletEndpoint /// when started the processor downloads does a download - validate - scan cycle until it reaches latest height on the blockchain. actor CompactBlockProcessor { - typealias EventClosure = (Event) async -> Void + // It would be better to use Combine here but Combine doesn't work great with async. When this runs regularly only one closure is stored here + // and that is one provided by `SDKSynchronizer`. But while running tests more "subscribers" is required here. Therefore it's required to handle + // more closures here. + private var eventClosures: [String: EventClosure] = [:] - enum Event { - /// Event sent when the CompactBlockProcessor presented an error. - case failed (Error) + private var syncTask: Task? - /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height - case finished (_ lastScannedHeight: BlockHeight, _ foundBlocks: Bool) + private let actions: [CBPState: Action] + private var context: ActionContext - /// Event sent when the CompactBlockProcessor found a newly mined transaction - case minedTransaction(ZcashTransaction.Overview) + private(set) var config: Configuration + private let configProvider: ConfigProvider + private var afterSyncHooksManager = AfterSyncHooksManager() - /// Event sent when the CompactBlockProcessor enhanced a bunch of transactions in some range. - case foundTransactions ([ZcashTransaction.Overview], CompactBlockRange) - - /// Event sent when the CompactBlockProcessor handled a ReOrg. - /// `reorgHeight` is the height on which the reorg was detected. - /// `rewindHeight` is the height that the processor backed to in order to solve the Reorg. - case handledReorg (_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight) - - /// Event sent when progress of the sync process changes. - case progressUpdated (CompactBlockProgress) - - /// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them. - case storedUTXOs ((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])) - - /// Event sent when the CompactBlockProcessor starts enhancing of the transactions. - case startedEnhancing - - /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs. - case startedFetching - - /// Event sent when the CompactBlockProcessor starts syncing. - case startedSyncing - - /// Event sent when the CompactBlockProcessor stops syncing. - case stopped - } + private let accountRepository: AccountRepository + let blockDownloaderService: BlockDownloaderService + private let internalSyncProgress: InternalSyncProgress + private let latestBlocksDataProvider: LatestBlocksDataProvider + private let logger: Logger + private let metrics: SDKMetrics + private let rustBackend: ZcashRustBackendWelding + let service: LightWalletService + let storage: CompactBlockRepository + private let transactionRepository: TransactionRepository + private let fileManager: ZcashFileManager + private var retryAttempts: Int = 0 + private var backoffTimer: Timer? + private var consecutiveChainValidationErrors: Int = 0 + + private var compactBlockProgress: CompactBlockProgress = .zero + /// Compact Block Processor configuration /// /// - parameter fsBlockCacheRoot: absolute root path where the filesystem block cache will be stored. @@ -156,8 +59,8 @@ actor CompactBlockProcessor { let dataDb: URL let spendParamsURL: URL let outputParamsURL: URL - let downloadBatchSize: Int - let scanningBatchSize: Int + let enhanceBatchSize: Int + let batchSize: Int let retries: Int let maxBackoffInterval: TimeInterval let maxReorgSize = ZcashSDK.maxReorgSize @@ -171,7 +74,7 @@ actor CompactBlockProcessor { var blockPollInterval: TimeInterval { TimeInterval.random(in: ZcashSDK.defaultPollInterval / 2 ... ZcashSDK.defaultPollInterval * 1.5) } - + init( alias: ZcashSynchronizerAlias, cacheDbURL: URL? = nil, @@ -180,11 +83,11 @@ actor CompactBlockProcessor { spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, - downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch, + enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch, + batchSize: Int = ZcashSDK.DefaultBatchSize, retries: Int = ZcashSDK.defaultRetries, maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval, rewindDistance: Int = ZcashSDK.defaultRewindDistance, - scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch, walletBirthdayProvider: @escaping () -> BlockHeight, saplingActivation: BlockHeight, network: ZcashNetwork @@ -196,17 +99,16 @@ actor CompactBlockProcessor { self.outputParamsURL = outputParamsURL self.saplingParamsSourceURL = saplingParamsSourceURL self.network = network - self.downloadBatchSize = downloadBatchSize + self.enhanceBatchSize = enhanceBatchSize + self.batchSize = batchSize self.retries = retries self.maxBackoffInterval = maxBackoffInterval self.rewindDistance = rewindDistance - self.scanningBatchSize = scanningBatchSize self.walletBirthdayProvider = walletBirthdayProvider self.saplingActivation = saplingActivation self.cacheDbURL = cacheDbURL - assert(downloadBatchSize >= scanningBatchSize) } - + init( alias: ZcashSynchronizerAlias, fsBlockCacheRoot: URL, @@ -214,11 +116,11 @@ actor CompactBlockProcessor { spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, - downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch, + enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch, + batchSize: Int = ZcashSDK.DefaultBatchSize, retries: Int = ZcashSDK.defaultRetries, maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval, rewindDistance: Int = ZcashSDK.defaultRewindDistance, - scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch, walletBirthdayProvider: @escaping () -> BlockHeight, network: ZcashNetwork ) { @@ -232,124 +134,21 @@ actor CompactBlockProcessor { self.saplingActivation = network.constants.saplingActivationHeight self.network = network self.cacheDbURL = nil - self.downloadBatchSize = downloadBatchSize + self.enhanceBatchSize = enhanceBatchSize + self.batchSize = batchSize self.retries = retries self.maxBackoffInterval = maxBackoffInterval self.rewindDistance = rewindDistance - self.scanningBatchSize = scanningBatchSize - - assert(downloadBatchSize >= scanningBatchSize) } } - /** - Represents the possible states of a CompactBlockProcessor - */ - enum State { - /** - connected and downloading blocks - */ - case syncing - - /** - was doing something but was paused - */ - case stopped - - /** - Processor is Enhancing transactions - */ - case enhancing - - /** - fetching utxos - */ - case fetching - - /** - was processing but erred - */ - case error(_ error: Error) - - /// Download sapling param files if needed. - case handlingSaplingFiles - - /** - Processor is up to date with the blockchain and you can now make transactions. - */ - case synced - } - - private var afterSyncHooksManager = AfterSyncHooksManager() - - let metrics: SDKMetrics - let logger: Logger - - /// Don't update this variable directly. Use `updateState()` method. - var state: State = .stopped - - private(set) var config: Configuration - - var maxAttemptsReached: Bool { - self.retryAttempts >= self.config.retries - } - - var shouldStart: Bool { - switch self.state { - case .stopped, .synced, .error: - return !maxAttemptsReached - default: - return false - } - } - - // It would be better to use Combine here but Combine doesn't work great with async. When this runs regularly only one closure is stored here - // and that is one provided by `SDKSynchronizer`. But while running tests more "subscribers" is required here. Therefore it's required to handle - // more closures here. - var eventClosures: [String: EventClosure] = [:] - - let blockDownloaderService: BlockDownloaderService - let blockDownloader: BlockDownloader - let blockValidator: BlockValidator - let blockScanner: BlockScanner - let blockEnhancer: BlockEnhancer - let utxoFetcher: UTXOFetcher - let saplingParametersHandler: SaplingParametersHandler - private let latestBlocksDataProvider: LatestBlocksDataProvider - - let service: LightWalletService - let storage: CompactBlockRepository - let transactionRepository: TransactionRepository - let accountRepository: AccountRepository - let rustBackend: ZcashRustBackendWelding - private var retryAttempts: Int = 0 - private var backoffTimer: Timer? - private var lastChainValidationFailure: BlockHeight? - private var consecutiveChainValidationErrors: Int = 0 - var processingError: Error? - private var foundBlocks = false - private var maxAttempts: Int { - config.retries - } - - var batchSize: BlockHeight { - BlockHeight(self.config.downloadBatchSize) - } - - private var cancelableTask: Task? - - private let internalSyncProgress: InternalSyncProgress - /// Initializes a CompactBlockProcessor instance /// - Parameters: /// - service: concrete implementation of `LightWalletService` protocol /// - storage: concrete implementation of `CompactBlockRepository` protocol /// - backend: a class that complies to `ZcashRustBackendWelding` /// - config: `Configuration` struct for this processor - init( - container: DIContainer, - config: Configuration - ) { + init(container: DIContainer, config: Configuration) { self.init( container: container, config: config, @@ -376,171 +175,139 @@ actor CompactBlockProcessor { accountRepository: initializer.accountRepository ) } - - internal init( - container: DIContainer, - config: Configuration, - accountRepository: AccountRepository - ) { + + init(container: DIContainer, config: Configuration, accountRepository: AccountRepository) { Dependencies.setupCompactBlockProcessor( in: container, config: config, accountRepository: accountRepository ) + let configProvider = ConfigProvider(config: config) + context = ActionContext(state: .idle) + actions = Self.makeActions(container: container, configProvider: configProvider) + self.metrics = container.resolve(SDKMetrics.self) self.logger = container.resolve(Logger.self) self.latestBlocksDataProvider = container.resolve(LatestBlocksDataProvider.self) self.internalSyncProgress = container.resolve(InternalSyncProgress.self) self.blockDownloaderService = container.resolve(BlockDownloaderService.self) - self.blockDownloader = container.resolve(BlockDownloader.self) - self.blockValidator = container.resolve(BlockValidator.self) - self.blockScanner = container.resolve(BlockScanner.self) - self.blockEnhancer = container.resolve(BlockEnhancer.self) - self.utxoFetcher = container.resolve(UTXOFetcher.self) - self.saplingParametersHandler = container.resolve(SaplingParametersHandler.self) self.service = container.resolve(LightWalletService.self) self.rustBackend = container.resolve(ZcashRustBackendWelding.self) self.storage = container.resolve(CompactBlockRepository.self) self.config = config self.transactionRepository = container.resolve(TransactionRepository.self) self.accountRepository = accountRepository - } - - deinit { - cancelableTask?.cancel() + self.fileManager = container.resolve(ZcashFileManager.self) + self.configProvider = configProvider } + deinit { + syncTask?.cancel() + syncTask = nil + } + + // swiftlint:disable:next cyclomatic_complexity + private static func makeActions(container: DIContainer, configProvider: ConfigProvider) -> [CBPState: Action] { + let actionsDefinition = CBPState.allCases.compactMap { state -> (CBPState, Action)? in + let action: Action + switch state { + case .migrateLegacyCacheDB: + action = MigrateLegacyCacheDBAction(container: container, configProvider: configProvider) + case .validateServer: + action = ValidateServerAction(container: container, configProvider: configProvider) + case .computeSyncRanges: + action = ComputeSyncRangesAction(container: container, configProvider: configProvider) + case .checksBeforeSync: + action = ChecksBeforeSyncAction(container: container) + case .download: + action = DownloadAction(container: container, configProvider: configProvider) + case .validate: + action = ValidateAction(container: container) + case .scan: + action = ScanAction(container: container, configProvider: configProvider) + case .clearAlreadyScannedBlocks: + action = ClearAlreadyScannedBlocksAction(container: container) + case .enhance: + action = EnhanceAction(container: container, configProvider: configProvider) + case .fetchUTXO: + action = FetchUTXOsAction(container: container) + case .handleSaplingParams: + action = SaplingParamsAction(container: container) + case .clearCache: + action = ClearCacheAction(container: container) + case .finished, .failed, .stopped, .idle: + return nil + } + + return (state, action) + } + + return Dictionary(uniqueKeysWithValues: actionsDefinition) + } + + // This is currently used only in tests. And it should be used only in tests. func update(config: Configuration) async { self.config = config - await stop() + await configProvider.update(config: config) } +} - func updateState(_ newState: State) async -> Void { - let oldState = state - state = newState - await transitionState(from: oldState, to: newState) - } +// MARK: - "Public" API - func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async { - eventClosures[identifier] = closure - } - - func send(event: Event) async { - for item in eventClosures { - await item.value(event) - } - } - - static func validateServerInfo( - _ info: LightWalletdInfo, - saplingActivation: BlockHeight, - localNetwork: ZcashNetwork, - rustBackend: ZcashRustBackendWelding - ) async throws { - // check network types - guard let remoteNetworkType = NetworkType.forChainName(info.chainName) else { - throw ZcashError.compactBlockProcessorChainName(info.chainName) - } - - guard remoteNetworkType == localNetwork.networkType else { - throw ZcashError.compactBlockProcessorNetworkMismatch(localNetwork.networkType, remoteNetworkType) - } - - guard saplingActivation == info.saplingActivationHeight else { - throw ZcashError.compactBlockProcessorSaplingActivationMismatch(saplingActivation, BlockHeight(info.saplingActivationHeight)) - } - - // check branch id - let localBranch = try rustBackend.consensusBranchIdFor(height: Int32(info.blockHeight)) - - guard let remoteBranchID = ConsensusBranchID.fromString(info.consensusBranchID) else { - throw ZcashError.compactBlockProcessorConsensusBranchID - } - - guard remoteBranchID == localBranch else { - throw ZcashError.compactBlockProcessorWrongConsensusBranchId(localBranch, remoteBranchID) - } - } - - /// Starts the CompactBlockProcessor instance and starts downloading and processing blocks - /// - /// triggers the blockProcessorStartedDownloading notification - /// - /// - Important: subscribe to the notifications before calling this method +extension CompactBlockProcessor { func start(retry: Bool = false) async { if retry { self.retryAttempts = 0 - self.processingError = nil self.backoffTimer?.invalidate() self.backoffTimer = nil } - guard shouldStart else { - switch self.state { - case .error(let error): - // max attempts have been reached - logger.info("max retry attempts reached with error: \(error)") - await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts)) - await updateState(.stopped) - case .stopped: - // max attempts have been reached - logger.info("max retry attempts reached") - await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts)) - case .synced: - // max attempts have been reached - logger.warn("max retry attempts reached on synced state, this indicates malfunction") - await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts)) - case .syncing, .enhancing, .fetching, .handlingSaplingFiles: + guard await canStartSync() else { + if await isIdle() { + logger.warn("max retry attempts reached on \(await context.state) state") + await send(event: .failed(ZcashError.compactBlockProcessorMaxAttemptsReached(config.retries))) + } else { logger.debug("Warning: compact block processor was started while busy!!!!") afterSyncHooksManager.insert(hook: .anotherSync) } return } - do { - if let legacyCacheDbURL = self.config.cacheDbURL { - try await self.migrateCacheDb(legacyCacheDbURL) - } - } catch { - await self.fail(error) + syncTask = Task(priority: .userInitiated) { + await run() } - - await self.nextBatch() } - /** - Stops the CompactBlockProcessor - - Note: retry count is reset - */ func stop() async { + syncTask?.cancel() self.backoffTimer?.invalidate() self.backoffTimer = nil - - cancelableTask?.cancel() - await blockDownloader.stopDownload() - - self.retryAttempts = 0 + await stopAllActions() + retryAttempts = 0 } - // MARK: Rewind + func latestHeight() async throws -> BlockHeight { + try await blockDownloaderService.latestBlockHeight() + } +} +// MARK: - Rewind + +extension CompactBlockProcessor { /// Rewinds to provided height. /// - Parameter height: height to rewind to. If nil is provided, it will rescan to nearest height (quick rescan) /// /// - Note: If this is called while sync is in progress then the sync process is stopped first and then rewind is executed. func rewind(context: AfterSyncHooksManager.RewindContext) async throws { logger.debug("Starting rewind") - switch self.state { - case .syncing, .enhancing, .fetching, .handlingSaplingFiles: + if await isIdle() { + logger.debug("Sync doesn't run. Executing rewind.") + try await doRewind(context: context) + } else { logger.debug("Stopping sync because of rewind") afterSyncHooksManager.insert(hook: .rewind(context)) await stop() - - case .stopped, .error, .synced: - logger.debug("Sync doesn't run. Executing rewind.") - try await doRewind(context: context) } } @@ -553,7 +320,7 @@ actor CompactBlockProcessor { do { nearestHeight = try await rustBackend.getNearestRewindHeight(height: height) } catch { - await fail(error) + await failure(error) return await context.completion(.failure(error)) } @@ -563,7 +330,7 @@ actor CompactBlockProcessor { do { try await rustBackend.rewindToHeight(height: rewindHeight) } catch { - await fail(error) + await failure(error) return await context.completion(.failure(error)) } @@ -577,23 +344,22 @@ actor CompactBlockProcessor { try await internalSyncProgress.rewind(to: rewindBlockHeight) - self.lastChainValidationFailure = nil await context.completion(.success(rewindBlockHeight)) } +} - // MARK: Wipe +// MARK: - Wipe +extension CompactBlockProcessor { func wipe(context: AfterSyncHooksManager.WipeContext) async throws { logger.debug("Starting wipe") - switch self.state { - case .syncing, .enhancing, .fetching, .handlingSaplingFiles: + if await isIdle() { + logger.debug("Sync doesn't run. Executing wipe.") + try await doWipe(context: context) + } else { logger.debug("Stopping sync because of wipe") afterSyncHooksManager.insert(hook: .wipe(context)) await stop() - - case .stopped, .error, .synced: - logger.debug("Sync doesn't run. Executing wipe.") - try await doWipe(context: context) } } @@ -601,8 +367,6 @@ actor CompactBlockProcessor { logger.debug("Executing wipe.") context.prewipe() - await updateState(.stopped) - do { try await self.storage.clear() try await internalSyncProgress.rewind(to: 0) @@ -620,405 +384,381 @@ actor CompactBlockProcessor { } } - // MARK: Sync + private func wipeLegacyCacheDbIfNeeded() { + guard let cacheDbURL = config.cacheDbURL else { return } + guard fileManager.isDeletableFile(atPath: cacheDbURL.pathExtension) else { return } + try? fileManager.removeItem(at: cacheDbURL) + } +} - func validateServer() async { - do { - let info = try await self.service.getInfo() - try await Self.validateServerInfo( - info, - saplingActivation: self.config.saplingActivation, - localNetwork: self.config.network, - rustBackend: self.rustBackend - ) - } catch { - await self.severeFailure(error) +// MARK: - Events + +extension CompactBlockProcessor { + typealias EventClosure = (Event) async -> Void + + enum Event { + /// Event sent when the CompactBlockProcessor presented an error. + case failed(Error) + + /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height + case finished(_ lastScannedHeight: BlockHeight) + + /// Event sent when the CompactBlockProcessor found a newly mined transaction + case minedTransaction(ZcashTransaction.Overview) + + /// Event sent when the CompactBlockProcessor enhanced a bunch of transactions in some range. + case foundTransactions([ZcashTransaction.Overview], CompactBlockRange) + + /// Event sent when the CompactBlockProcessor handled a ReOrg. + /// `reorgHeight` is the height on which the reorg was detected. + /// `rewindHeight` is the height that the processor backed to in order to solve the Reorg. + case handledReorg(_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight) + + /// Event sent when progress of some specific action happened. + case progressPartialUpdate(CompactBlockProgressUpdate) + + /// Event sent when progress of the sync process changes. + case progressUpdated(Float) + + /// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them. + case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])) + + /// Event sent when the CompactBlockProcessor starts enhancing of the transactions. + case startedEnhancing + + /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs. + case startedFetching + + /// Event sent when the CompactBlockProcessor starts syncing. + case startedSyncing + + /// Event sent when the CompactBlockProcessor stops syncing. + case stopped + } + + func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async { + eventClosures[identifier] = closure + } + + private func send(event: Event) async { + for item in eventClosures { + await item.value(event) } } - - /// Processes new blocks on the given range based on the configuration set for this instance +} + +// MARK: - Main loop + +extension CompactBlockProcessor { + // This is main loop of the sync process. It simply takes state and try to find action which handles it. If action is found it executes the + // action. If action is not found then loop finishes. Thanks to this it's super easy to identify start point of sync process and end points + // of sync process without any side effects. + // + // Check `docs/cbp_state_machine.puml` file and `docs/images/cbp_state_machine.png` file to see all the state tree. Also when you update state + // tree in the code update this documentation. Image is generated by plantuml tool. + // // swiftlint:disable:next cyclomatic_complexity - func processNewBlocks(ranges: SyncRanges) async { - self.foundBlocks = true - - cancelableTask = Task(priority: .userInitiated) { - do { - let totalProgressRange = computeTotalProgressRange(from: ranges) + private func run() async { + logger.debug("Starting run") + await resetContext() - logger.debug(""" - Syncing with ranges: - downloaded but not scanned: \ - \(ranges.downloadedButUnscannedRange?.lowerBound ?? -1)...\(ranges.downloadedButUnscannedRange?.upperBound ?? -1) - download and scan: \(ranges.downloadAndScanRange?.lowerBound ?? -1)...\(ranges.downloadAndScanRange?.upperBound ?? -1) - enhance range: \(ranges.enhanceRange?.lowerBound ?? -1)...\(ranges.enhanceRange?.upperBound ?? -1) - fetchUTXO range: \(ranges.fetchUTXORange?.lowerBound ?? -1)...\(ranges.fetchUTXORange?.upperBound ?? -1) - total progress range: \(totalProgressRange.lowerBound)...\(totalProgressRange.upperBound) - """) + while true { + // Sync is starting when the state is `idle`. + if await context.state == .idle { + // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory. + await stopAllActions() + // Update state to the first state in state machine that can be handled by action. + await context.update(state: .migrateLegacyCacheDB) + await syncStarted() - var anyActionExecuted = false + if backoffTimer == nil { + await setTimer() + } + } - // clear any present cached state if needed. - // this checks if there was a sync in progress that was - // interrupted abruptly and cache was not able to be cleared - // properly and internal state set to the appropriate value - if let newLatestDownloadedHeight = ranges.shouldClearBlockCacheAndUpdateInternalState() { - try await storage.clear() - try await internalSyncProgress.set(newLatestDownloadedHeight, .latestDownloadedBlockHeight) + let state = await context.state + logger.debug("Handling state: \(state)") + + // Try to find action for state. + guard let action = actions[state] else { + // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory. + await stopAllActions() + if await syncFinished() { + await resetContext() + continue } else { - try await storage.create() + break } + } - if let range = ranges.downloadedButUnscannedRange { - logger.debug("Starting scan with downloaded but not scanned blocks with range: \(range.lowerBound)...\(range.upperBound)") - try await blockScanner.scanBlocks(at: range, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in - let progress = BlockProgress( - startHeight: totalProgressRange.lowerBound, - targetHeight: totalProgressRange.upperBound, - progressHeight: lastScannedHeight - ) - await self?.notifyProgress(.syncing(progress)) - } - } + do { + try Task.checkCancellation() - if let range = ranges.downloadAndScanRange { - logger.debug("Starting sync with range: \(range.lowerBound)...\(range.upperBound)") - try await blockDownloader.setSyncRange(range, batchSize: batchSize) - try await downloadAndScanBlocks(at: range, totalProgressRange: totalProgressRange) - // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory. - await blockDownloader.stopDownload() - } - - if let range = ranges.enhanceRange { - anyActionExecuted = true - logger.debug("Enhancing with range: \(range.lowerBound)...\(range.upperBound)") - await updateState(.enhancing) - if let transactions = try await blockEnhancer.enhance( - at: range, - didEnhance: { [weak self] progress in - await self?.notifyProgress(.enhance(progress)) - if - let foundTx = progress.lastFoundTransaction, - progress.newlyMined { - await self?.notifyMinedTransaction(foundTx) - } + // Execute action. + context = try await action.run(with: context) { [weak self] event in + await self?.send(event: event) + if let progressChanged = await self?.compactBlockProgress.event(event), progressChanged { + if let progress = await self?.compactBlockProgress.progress { + await self?.send(event: .progressUpdated(progress)) } - ) { - await notifyTransactions(transactions, in: range) } } - if let range = ranges.fetchUTXORange { - anyActionExecuted = true - logger.debug("Fetching UTXO with range: \(range.lowerBound)...\(range.upperBound)") - await updateState(.fetching) - let result = try await utxoFetcher.fetch(at: range) { [weak self] progress in - await self?.notifyProgress(.fetch(progress)) - } - await send(event: .storedUTXOs(result)) - } - - logger.debug("Fetching sapling parameters") - await updateState(.handlingSaplingFiles) - try await saplingParametersHandler.handleIfNeeded() - - logger.debug("Clearing cache") - try await clearCompactBlockCache() - - if !Task.isCancelled { - let newBlocksMined = await ranges.latestBlockHeight < latestBlocksDataProvider.latestBlockHeight - await processBatchFinished(height: (anyActionExecuted && !newBlocksMined) ? ranges.latestBlockHeight : nil) - } + await didFinishAction() } catch { // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory. - await blockDownloader.stopDownload() + await stopAllActions() logger.error("Sync failed with error: \(error)") if Task.isCancelled { logger.info("Processing cancelled.") - await updateState(.stopped) do { - try await handleAfterSyncHooks() + if try await syncTaskWasCancelled() { + // Start sync all over again + await resetContext() + } else { + // end the sync loop + break + } } catch { - await fail(error) + await failure(error) + break } } else { - if case let ZcashError.rustValidateCombinedChainInvalidChain(height) = error { - await validationFailed(at: BlockHeight(height)) + if await handleSyncFailure(action: action, error: error) { + // Start sync all over again + await resetContext() } else { - logger.error("processing failed with error: \(error)") - await fail(error) + // end the sync loop + break } } } } + + logger.debug("Run ended") + syncTask = nil } - private func handleAfterSyncHooks() async throws { + private func syncTaskWasCancelled() async throws -> Bool { + logger.info("Sync cancelled.") + await context.update(state: .stopped) + await send(event: .stopped) + return try await handleAfterSyncHooks() + } + + private func handleSyncFailure(action: Action, error: Error) async -> Bool { + if action.removeBlocksCacheWhenFailed { + await ifTaskIsNotCanceledClearCompactBlockCache() + } + + if case let ZcashError.rustValidateCombinedChainInvalidChain(height) = error { + logger.error("Sync failed because of validation error: \(error)") + do { + try await validationFailed(at: BlockHeight(height)) + // Start sync all over again + return true + } catch { + await failure(error) + return false + } + } else { + logger.error("Sync failed with error: \(error)") + await failure(error) + return false + } + } + + // swiftlint:disable:next cyclomatic_complexity + private func didFinishAction() async { + // This is evalution of the state setup by previous action. + switch await context.state { + case .idle: + break + case .migrateLegacyCacheDB: + break + case .validateServer: + break + case .computeSyncRanges: + break + case .checksBeforeSync: + break + case .download: + break + case .validate: + break + case .scan: + break + case .clearAlreadyScannedBlocks: + break + case .enhance: + await send(event: .startedEnhancing) + case .fetchUTXO: + await send(event: .startedFetching) + case .handleSaplingParams: + break + case .clearCache: + break + case .finished: + break + case .failed: + break + case .stopped: + break + } + } + + private func resetContext() async { + context = ActionContext(state: .idle) + await compactBlockProgress.reset() + } + + private func syncStarted() async { + logger.debug("Sync started") + // handle start of the sync process + await send(event: .startedSyncing) + } + + private func syncFinished() async -> Bool { + logger.debug("Sync finished") + let latestBlockHeightWhenSyncing = await context.syncRanges.latestBlockHeight + let latestBlockHeight = await latestBlocksDataProvider.latestBlockHeight + // If `latestBlockHeightWhenSyncing` is 0 then it means that there was nothing to sync in last sync process. + let newerBlocksWereMinedDuringSync = + latestBlockHeightWhenSyncing > 0 && latestBlockHeightWhenSyncing < latestBlockHeight + + retryAttempts = 0 + consecutiveChainValidationErrors = 0 + + let lastScannedHeight = await latestBlocksDataProvider.latestScannedHeight + // Some actions may not run. For example there are no transactions to enhance and therefore there is no enhance progress. And in + // cases like this computation of final progress won't work properly. So let's fake 100% progress at the end of the sync process. + await send(event: .progressUpdated(1)) + await send(event: .finished(lastScannedHeight)) + await context.update(state: .finished) + + // If new blocks were mined during previous sync run the sync process again + if newerBlocksWereMinedDuringSync { + return true + } else { + await setTimer() + return false + } + } + + private func validationFailed(at height: BlockHeight) async throws { + // rewind + let rewindHeight = determineLowerBound( + errorHeight: height, + consecutiveErrors: consecutiveChainValidationErrors, + walletBirthday: config.walletBirthday + ) + + consecutiveChainValidationErrors += 1 + + try await rustBackend.rewindToHeight(height: Int32(rewindHeight)) + + try await blockDownloaderService.rewind(to: rewindHeight) + try await internalSyncProgress.rewind(to: rewindHeight) + + await send(event: .handledReorg(height, rewindHeight)) + } + + private func failure(_ error: Error) async { + await context.update(state: .failed) + + logger.error("Fail with error: \(error)") + + self.retryAttempts += 1 + await send(event: .failed(error)) + + // don't set a new timer if there are no more attempts. + if hasRetryAttempt() { + await self.setTimer() + } + } + + private func handleAfterSyncHooks() async throws -> Bool { let afterSyncHooksManager = self.afterSyncHooksManager self.afterSyncHooksManager = AfterSyncHooksManager() if let wipeContext = afterSyncHooksManager.shouldExecuteWipeHook() { try await doWipe(context: wipeContext) + return false } else if let rewindContext = afterSyncHooksManager.shouldExecuteRewindHook() { try await doRewind(context: rewindContext) + return false } else if afterSyncHooksManager.shouldExecuteAnotherSyncHook() { logger.debug("Starting new sync.") - await nextBatch() + return true + } else { + return false } } +} - private func downloadAndScanBlocks(at range: CompactBlockRange, totalProgressRange: CompactBlockRange) async throws { - // Divide `range` by `batchSize` and compute how many time do we need to run to download and scan all the blocks. - // +1 must be done here becase `range` is closed range. So even if upperBound and lowerBound are same there is one block to sync. - let blocksCountToSync = (range.upperBound - range.lowerBound) + 1 - var loopsCount = blocksCountToSync / batchSize - if blocksCountToSync % batchSize != 0 { - loopsCount += 1 - } +// MARK: - Utils - var lastScannedHeight: BlockHeight = .zero - for i in 0.. CompactBlockRange { - let lowerBound = fullRange.lowerBound + (loopCounter * batchSize) - let upperBound = min(fullRange.lowerBound + ((loopCounter + 1) * batchSize) - 1, fullRange.upperBound) - return lowerBound...upperBound + private func isIdle() async -> Bool { + return syncTask == nil } - /// It may happen that sync process start with syncing blocks that were downloaded but not synced in previous run of the sync process. This - /// methods analyses what must be done and computes range that should be used to compute reported progress. - private func computeTotalProgressRange(from syncRanges: SyncRanges) -> CompactBlockRange { - guard syncRanges.downloadedButUnscannedRange != nil || syncRanges.downloadAndScanRange != nil else { - // In this case we are sure that no downloading or scanning happens so this returned range won't be even used. And it's easier to return - // this "fake" range than to handle nil. - return 0...0 - } - - // Thanks to guard above we can be sure that one of these two ranges is not nil. - let lowerBound = syncRanges.downloadedButUnscannedRange?.lowerBound ?? syncRanges.downloadAndScanRange?.lowerBound ?? 0 - let upperBound = syncRanges.downloadAndScanRange?.upperBound ?? syncRanges.downloadedButUnscannedRange?.upperBound ?? 0 - - return lowerBound...upperBound + private func canStartSync() async -> Bool { + return await isIdle() && hasRetryAttempt() } - func notifyMinedTransaction(_ tx: ZcashTransaction.Overview) async { - logger.debug("notify mined transaction: \(tx)") - await send(event: .minedTransaction(tx)) + private func hasRetryAttempt() -> Bool { + retryAttempts < config.retries } - func notifyProgress(_ progress: CompactBlockProgress) async { - logger.debug("progress: \(progress)") - await send(event: .progressUpdated(progress)) - } - - func notifyTransactions(_ txs: [ZcashTransaction.Overview], in range: CompactBlockRange) async { - await send(event: .foundTransactions(txs, range)) - } - - func determineLowerBound( - errorHeight: Int, - consecutiveErrors: Int, - walletBirthday: BlockHeight - ) -> BlockHeight { + func determineLowerBound(errorHeight: Int, consecutiveErrors: Int, walletBirthday: BlockHeight) -> BlockHeight { let offset = min(ZcashSDK.maxReorgSize, ZcashSDK.defaultRewindDistance * (consecutiveErrors + 1)) return max(errorHeight - offset, walletBirthday - ZcashSDK.maxReorgSize) } - func severeFailure(_ error: Error) async { - cancelableTask?.cancel() - await blockDownloader.stopDownload() - logger.error("show stopper failure: \(error)") - self.backoffTimer?.invalidate() - self.retryAttempts = config.retries - self.processingError = error - await updateState(.error(error)) - await self.notifyError(error) - } - - func fail(_ error: Error) async { - // TODO: [#713] specify: failure. https://github.com/zcash/ZcashLightClientKit/issues/713 - logger.error("\(error)") - cancelableTask?.cancel() - await blockDownloader.stopDownload() - self.retryAttempts += 1 - self.processingError = error - switch self.state { - case .error: - await notifyError(error) - default: - break - } - await updateState(.error(error)) - guard self.maxAttemptsReached else { return } - // don't set a new timer if there are no more attempts. - await self.setTimer() - } - - private func validateConfiguration() throws { - guard FileManager.default.isReadableFile(atPath: config.fsBlockCacheRoot.absoluteString) else { - throw ZcashError.compactBlockProcessorMissingDbPath(config.fsBlockCacheRoot.absoluteString) - } - - guard FileManager.default.isReadableFile(atPath: config.dataDb.absoluteString) else { - throw ZcashError.compactBlockProcessorMissingDbPath(config.dataDb.absoluteString) + private func stopAllActions() async { + for action in actions.values { + await action.stop() } } - private func nextBatch() async { - await updateState(.syncing) - if backoffTimer == nil { await setTimer() } - do { - let nextState = try await NextStateHelper.nextState( - service: self.service, - downloaderService: blockDownloaderService, - latestBlocksDataProvider: latestBlocksDataProvider, - config: self.config, - rustBackend: self.rustBackend, - internalSyncProgress: internalSyncProgress, - alias: config.alias - ) - switch nextState { - case .finishProcessing(let height): - await self.processingFinished(height: height) - case .processNewBlocks(let ranges): - await self.processNewBlocks(ranges: ranges) - case let .wait(latestHeight, latestDownloadHeight): - // Lightwalletd might be syncing - logger.info( - "Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight) " + - "while latest blockheight is reported at: \(latestHeight)" - ) - await self.processingFinished(height: latestDownloadHeight) - } - } catch { - await self.severeFailure(error) - } - } - - internal func validationFailed(at height: BlockHeight) async { - // cancel all Tasks - cancelableTask?.cancel() - await blockDownloader.stopDownload() - - // register latest failure - self.lastChainValidationFailure = height - - // rewind - let rewindHeight = determineLowerBound( - errorHeight: height, - consecutiveErrors: consecutiveChainValidationErrors, - walletBirthday: self.config.walletBirthday - ) - - self.consecutiveChainValidationErrors += 1 - - do { - try await rustBackend.rewindToHeight(height: Int32(rewindHeight)) - } catch { - await fail(error) - return - } - - do { - try await blockDownloaderService.rewind(to: rewindHeight) - try await internalSyncProgress.rewind(to: rewindHeight) - - await send(event: .handledReorg(height, rewindHeight)) - - // process next batch - await self.nextBatch() - } catch { - await self.fail(error) - } - } - - internal func processBatchFinished(height: BlockHeight?) async { - retryAttempts = 0 - consecutiveChainValidationErrors = 0 - - if let height { - await processingFinished(height: height) - } else { - await nextBatch() - } - } - - private func processingFinished(height: BlockHeight) async { - await send(event: .finished(height, foundBlocks)) - await updateState(.synced) - await setTimer() - } - - private func ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: BlockHeight) async { + private func ifTaskIsNotCanceledClearCompactBlockCache() async { guard !Task.isCancelled else { return } + let lastScannedHeight = await latestBlocksDataProvider.latestScannedHeight do { // Blocks download work in parallel with scanning. So imagine this scenario: // @@ -1038,98 +778,11 @@ actor CompactBlockProcessor { } } - private func clearCompactBlockCache(upTo height: BlockHeight) async throws { - try await storage.clear(upTo: height) - logger.info("Cache removed upTo \(height)") - } - private func clearCompactBlockCache() async throws { - await blockDownloader.stopDownload() + await stopAllActions() try await storage.clear() logger.info("Cache removed") } - - private func setTimer() async { - let interval = self.config.blockPollInterval - self.backoffTimer?.invalidate() - let timer = Timer( - timeInterval: interval, - repeats: true, - block: { [weak self] _ in - Task { [weak self] in - guard let self else { return } - switch await self.state { - case .syncing, .enhancing, .fetching, .handlingSaplingFiles: - await self.latestBlocksDataProvider.updateBlockData() - case .stopped, .error, .synced: - if await self.shouldStart { - self.logger.debug( - """ - Timer triggered: Starting compact Block processor!. - Processor State: \(await self.state) - latestHeight: \(try await self.transactionRepository.lastScannedHeight()) - attempts: \(await self.retryAttempts) - """ - ) - await self.start() - } else if await self.maxAttemptsReached { - await self.fail(ZcashError.compactBlockProcessorMaxAttemptsReached(self.config.retries)) - } - } - } - } - ) - RunLoop.main.add(timer, forMode: .default) - - self.backoffTimer = timer - } - - private func transitionState(from oldValue: State, to newValue: State) async { - guard oldValue != newValue else { - return - } - - switch newValue { - case .error(let err): - await notifyError(err) - case .stopped: - await send(event: .stopped) - case .enhancing: - await send(event: .startedEnhancing) - case .fetching: - await send(event: .startedFetching) - case .handlingSaplingFiles: - // We don't report this to outside world as separate phase for now. - break - case .synced: - // transition to this state is handled by `processingFinished(height: BlockHeight)` - break - case .syncing: - await send(event: .startedSyncing) - } - } - - private func notifyError(_ err: Error) async { - await send(event: .failed(err)) - } - // TODO: [#713] encapsulate service errors better, https://github.com/zcash/ZcashLightClientKit/issues/713 -} - -extension CompactBlockProcessor.State: Equatable { - public static func == (lhs: CompactBlockProcessor.State, rhs: CompactBlockProcessor.State) -> Bool { - switch (lhs, rhs) { - case - (.syncing, .syncing), - (.stopped, .stopped), - (.error, .error), - (.synced, .synced), - (.enhancing, .enhancing), - (.fetching, .fetching): - return true - default: - return false - } - } } extension CompactBlockProcessor { @@ -1204,170 +857,17 @@ extension CompactBlockProcessor { } } -extension CompactBlockProcessor { - enum NextState: Equatable { - case finishProcessing(height: BlockHeight) - case processNewBlocks(ranges: SyncRanges) - case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight) - } - - @discardableResult - func figureNextBatch( - downloaderService: BlockDownloaderService - ) async throws -> NextState { - try Task.checkCancellation() - - do { - return try await CompactBlockProcessor.NextStateHelper.nextState( - service: service, - downloaderService: downloaderService, - latestBlocksDataProvider: latestBlocksDataProvider, - config: config, - rustBackend: rustBackend, - internalSyncProgress: internalSyncProgress, - alias: config.alias - ) - } catch { - throw error - } - } -} +// MARK: - Config provider extension CompactBlockProcessor { - enum NextStateHelper { - // swiftlint:disable:next function_parameter_count - static func nextState( - service: LightWalletService, - downloaderService: BlockDownloaderService, - latestBlocksDataProvider: LatestBlocksDataProvider, - config: Configuration, - rustBackend: ZcashRustBackendWelding, - internalSyncProgress: InternalSyncProgress, - alias: ZcashSynchronizerAlias - ) async throws -> CompactBlockProcessor.NextState { - // It should be ok to not create new Task here because this method is already async. But for some reason something not good happens - // when Task is not created here. For example tests start failing. Reason is unknown at this time. - let task = Task(priority: .userInitiated) { - let info = try await service.getInfo() + actor ConfigProvider { + var config: Configuration + init(config: Configuration) { + self.config = config + } - try await CompactBlockProcessor.validateServerInfo( - info, - saplingActivation: config.saplingActivation, - localNetwork: config.network, - rustBackend: rustBackend - ) - - let latestDownloadHeight = try await downloaderService.lastDownloadedBlockHeight() - - try await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadHeight, alias: alias) - - await latestBlocksDataProvider.updateScannedData() - await latestBlocksDataProvider.updateBlockData() - - return try await internalSyncProgress.computeNextState( - latestBlockHeight: latestBlocksDataProvider.latestBlockHeight, - latestScannedHeight: latestBlocksDataProvider.latestScannedHeight, - walletBirthday: config.walletBirthday - ) - } - - return try await task.value + func update(config: Configuration) async { + self.config = config } } } - -/// This extension contains asociated types and functions needed to clean up the -/// `cacheDb` in favor of `FsBlockDb`. Once this cleanup functionality is deprecated, -/// delete the whole extension and reference to it in other parts of the code including tests. -extension CompactBlockProcessor { - public enum CacheDbMigrationError: Error { - case fsCacheMigrationFailedSameURL - case failedToDeleteLegacyDb(Error) - case failedToInitFsBlockDb(Error) - case failedToSetDownloadHeight(Error) - } - - /// Deletes the SQLite cacheDb and attempts to initialize the fsBlockDbRoot - /// - parameter legacyCacheDbURL: the URL where the cache Db used to be stored. - /// - Throws: `InitializerError.fsCacheInitFailedSameURL` when the given URL - /// is the same URL than the one provided as `self.fsBlockDbRoot` assuming that's a - /// programming error being the `legacyCacheDbURL` a sqlite database file and not a - /// directory. Also throws errors from initializing the fsBlockDbRoot. - /// - /// - Note: Errors from deleting the `legacyCacheDbURL` won't be throwns. - func migrateCacheDb(_ legacyCacheDbURL: URL) async throws { - guard legacyCacheDbURL != config.fsBlockCacheRoot else { - throw ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL - } - - // Instance with alias `default` is same as instance before the Alias was introduced. So it makes sense that only this instance handles - // legacy cache DB. Any instance with different than `default` alias was created after the Alias was introduced and at this point legacy - // cache DB is't anymore. So there is nothing to migrate for instances with not default Alias. - guard config.alias == .default else { - return - } - - // if the URL provided is not readable, it means that the client has a reference - // to the cacheDb file but it has been deleted in a prior sync cycle. there's - // nothing to do here. - guard FileManager.default.isReadableFile(atPath: legacyCacheDbURL.path) else { - return - } - - do { - // if there's a readable file at the provided URL, delete it. - try FileManager.default.removeItem(at: legacyCacheDbURL) - } catch { - throw ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb(error) - } - - // create the storage - try await self.storage.create() - - // The database has been deleted, so we have adjust the internal state of the - // `CompactBlockProcessor` so that it doesn't rely on download heights set - // by a previous processing cycle. - let lastScannedHeight = try await transactionRepository.lastScannedHeight() - - try await internalSyncProgress.set(lastScannedHeight, .latestDownloadedBlockHeight) - } - - func wipeLegacyCacheDbIfNeeded() { - guard let cacheDbURL = config.cacheDbURL else { - return - } - - guard FileManager.default.isDeletableFile(atPath: cacheDbURL.pathExtension) else { - return - } - - try? FileManager.default.removeItem(at: cacheDbURL) - } -} - -extension SyncRanges { - /// Tells whether the state represented by these sync ranges evidence some sort of - /// outdated state on the cache or the internal state of the compact block processor. - /// - /// - Note: this can mean that the processor has synced over the height that the internal - /// state knows of because the sync process was interrupted before it could reflect - /// it in the internal state storage. This could happen because of many factors, the - /// most feasible being OS shutting down a background process or the user abruptly - /// exiting the app. - /// - Returns: an ``Optional`` where Some represents what's the - /// new state the internal state should reflect and indicating that the cache should be cleared - /// as well. c`None` means that no action is required. - func shouldClearBlockCacheAndUpdateInternalState() -> BlockHeight? { - guard self.downloadedButUnscannedRange != nil else { - return nil - } - - guard - let latestScannedHeight = self.latestScannedHeight, - let latestDownloadedHeight = self.latestDownloadedBlockHeight, - latestScannedHeight > latestDownloadedHeight - else { return nil } - - return latestScannedHeight - } -} diff --git a/Sources/ZcashLightClientKit/Block/Download/BlockDownloader.swift b/Sources/ZcashLightClientKit/Block/Download/BlockDownloader.swift index 09b0e1c6..41000e3c 100644 --- a/Sources/ZcashLightClientKit/Block/Download/BlockDownloader.swift +++ b/Sources/ZcashLightClientKit/Block/Download/BlockDownloader.swift @@ -250,6 +250,7 @@ extension BlockDownloaderImpl: BlockDownloader { } func setSyncRange(_ range: CompactBlockRange, batchSize: Int) async throws { + guard range != syncRange else { return } downloadStream = nil self.batchSize = batchSize syncRange = range diff --git a/Sources/ZcashLightClientKit/Block/Enhance/BlockEnhancer.swift b/Sources/ZcashLightClientKit/Block/Enhance/BlockEnhancer.swift index c474db1c..f4a5abb0 100644 --- a/Sources/ZcashLightClientKit/Block/Enhance/BlockEnhancer.swift +++ b/Sources/ZcashLightClientKit/Block/Enhance/BlockEnhancer.swift @@ -7,8 +7,52 @@ import Foundation +public struct EnhancementProgress: Equatable { + /// total transactions that were detected in the `range` + public let totalTransactions: Int + /// enhanced transactions so far + public let enhancedTransactions: Int + /// last found transaction + public let lastFoundTransaction: ZcashTransaction.Overview? + /// block range that's being enhanced + public let range: CompactBlockRange + /// whether this transaction can be considered `newly mined` and not part of the + /// wallet catching up to stale and uneventful blocks. + public let newlyMined: Bool + + public init( + totalTransactions: Int, + enhancedTransactions: Int, + lastFoundTransaction: ZcashTransaction.Overview?, + range: CompactBlockRange, + newlyMined: Bool + ) { + self.totalTransactions = totalTransactions + self.enhancedTransactions = enhancedTransactions + self.lastFoundTransaction = lastFoundTransaction + self.range = range + self.newlyMined = newlyMined + } + + public var progress: Float { + totalTransactions > 0 ? Float(enhancedTransactions) / Float(totalTransactions) : 0 + } + + public static var zero: EnhancementProgress { + EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false) + } + + public static func == (lhs: EnhancementProgress, rhs: EnhancementProgress) -> Bool { + return + lhs.totalTransactions == rhs.totalTransactions && + lhs.enhancedTransactions == rhs.enhancedTransactions && + lhs.lastFoundTransaction?.id == rhs.lastFoundTransaction?.id && + lhs.range == rhs.range + } +} + protocol BlockEnhancer { - func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? + func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? } struct BlockEnhancerImpl { @@ -38,7 +82,7 @@ struct BlockEnhancerImpl { } extension BlockEnhancerImpl: BlockEnhancer { - func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? { + func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? { try Task.checkCancellation() logger.debug("Started Enhancing range: \(range)") diff --git a/Sources/ZcashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift b/Sources/ZcashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift index a38bb83a..7086bce9 100644 --- a/Sources/ZcashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift +++ b/Sources/ZcashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift @@ -19,7 +19,7 @@ struct UTXOFetcherConfig { protocol UTXOFetcher { func fetch( at range: CompactBlockRange, - didFetch: (Float) async -> Void + didFetch: @escaping (Float) async -> Void ) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]) } @@ -36,7 +36,7 @@ struct UTXOFetcherImpl { extension UTXOFetcherImpl: UTXOFetcher { func fetch( at range: CompactBlockRange, - didFetch: (Float) async -> Void + didFetch: @escaping (Float) async -> Void ) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]) { try Task.checkCancellation() diff --git a/Sources/ZcashLightClientKit/Block/Utils/CompactBlockProgress.swift b/Sources/ZcashLightClientKit/Block/Utils/CompactBlockProgress.swift new file mode 100644 index 00000000..78f68495 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Utils/CompactBlockProgress.swift @@ -0,0 +1,64 @@ +// +// CompactBlockProgress.swift +// +// +// Created by Michal Fousek on 11.05.2023. +// + +import Foundation + +final actor CompactBlockProgress { + static let zero = CompactBlockProgress() + + enum Action: Equatable { + case enhance + case fetch + case scan + + func weight() -> Float { + switch self { + case .enhance: return 0.08 + case .fetch: return 0.02 + case .scan: return 0.9 + } + } + } + + var actionProgresses: [Action: Float] = [:] + + var progress: Float { + var overallProgress = Float(0) + actionProgresses.forEach { key, value in + overallProgress += value * key.weight() + } + + return overallProgress + } + + func event(_ event: CompactBlockProcessor.Event) -> Bool { + guard case .progressPartialUpdate(let update) = event else { + return false + } + + switch update { + case .syncing(let progress): + actionProgresses[.scan] = progress.progress + case .enhance(let progress): + actionProgresses[.enhance] = progress.progress + case .fetch(let progress): + actionProgresses[.fetch] = progress + } + + return true + } + + func reset() { + actionProgresses.removeAll() + } +} + +enum CompactBlockProgressUpdate: Equatable { + case syncing(_ progress: BlockProgress) + case enhance(_ progress: EnhancementProgress) + case fetch(_ progress: Float) +} diff --git a/Sources/ZcashLightClientKit/Block/Utils/InternalSyncProgress.swift b/Sources/ZcashLightClientKit/Block/Utils/InternalSyncProgress.swift index 9c71529f..f7fa3887 100644 --- a/Sources/ZcashLightClientKit/Block/Utils/InternalSyncProgress.swift +++ b/Sources/ZcashLightClientKit/Block/Utils/InternalSyncProgress.swift @@ -9,20 +9,35 @@ import Foundation struct SyncRanges: Equatable { let latestBlockHeight: BlockHeight - /// The sync process can be interrupted in any phase. It may happen that it's interrupted while downloading blocks. In that case in next sync - /// process already downloaded blocks needs to be scanned before the sync process starts to download new blocks. And the range of blocks that are - /// already downloaded but not scanned is stored in this variable. - let downloadedButUnscannedRange: CompactBlockRange? - /// Range of blocks that are not yet downloaded and not yet scanned. - let downloadAndScanRange: CompactBlockRange? + // Range of blocks that are not yet downloaded + let downloadRange: CompactBlockRange? + /// Range of blocks that are not yet scanned. + let scanRange: CompactBlockRange? /// Range of blocks that are not enhanced yet. let enhanceRange: CompactBlockRange? /// Range of blocks for which no UTXOs are fetched yet. let fetchUTXORange: CompactBlockRange? let latestScannedHeight: BlockHeight? - let latestDownloadedBlockHeight: BlockHeight? + + static var empty: SyncRanges { + SyncRanges( + latestBlockHeight: 0, + downloadRange: nil, + scanRange: nil, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + } +} + +enum NextState: Equatable { + case finishProcessing(height: BlockHeight) + case processNewBlocks(ranges: SyncRanges) + case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight) } protocol InternalSyncProgressStorage { @@ -30,6 +45,7 @@ protocol InternalSyncProgressStorage { func bool(for key: String) async throws -> Bool func integer(for key: String) async throws -> Int func set(_ value: Int, for key: String) async throws + // sourcery: mockedName="setBool" func set(_ value: Bool, for key: String) async throws } @@ -132,7 +148,7 @@ actor InternalSyncProgress { latestBlockHeight: BlockHeight, latestScannedHeight: BlockHeight, walletBirthday: BlockHeight - ) async throws -> CompactBlockProcessor.NextState { + ) async throws -> NextState { let latestDownloadedBlockHeight = try await self.latestDownloadedBlockHeight let latestEnhancedHeight = try await self.latestEnhancedHeight let latestUTXOFetchedHeight = try await self.latestUTXOFetchedHeight @@ -174,15 +190,6 @@ actor InternalSyncProgress { let latestEnhancedHeight = try await self.latestEnhancedHeight let latestUTXOFetchedHeight = try await self.latestUTXOFetchedHeight - // If there is more downloaded then scanned blocks we have to range for these blocks. The sync process will then start with scanning these - // blocks instead of downloading new ones. - let downloadedButUnscannedRange: CompactBlockRange? - if latestScannedHeight < latestDownloadedBlockHeight { - downloadedButUnscannedRange = latestScannedHeight + 1...latestDownloadedBlockHeight - } else { - downloadedButUnscannedRange = nil - } - if latestScannedHeight > latestDownloadedBlockHeight { logger.warn(""" InternalSyncProgress found inconsistent state. @@ -191,24 +198,16 @@ actor InternalSyncProgress { latestScannedHeight: \(latestScannedHeight) latestEnhancedHeight: \(latestEnhancedHeight) latestUTXOFetchedHeight: \(latestUTXOFetchedHeight) - - latest downloaded height """) } - // compute the range that must be downloaded and scanned based on - // birthday, `latestDownloadedBlockHeight`, `latestScannedHeight` and - // latest block height fetched from the chain. - let downloadAndScanRange = computeRange( - latestHeight: max(latestDownloadedBlockHeight, latestScannedHeight), - birthday: birthday, - latestBlockHeight: latestBlockHeight - ) + let downloadRange = computeRange(latestHeight: latestDownloadedBlockHeight, birthday: birthday, latestBlockHeight: latestBlockHeight) + let scanRange = computeRange(latestHeight: latestScannedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight) return SyncRanges( latestBlockHeight: latestBlockHeight, - downloadedButUnscannedRange: downloadedButUnscannedRange, - downloadAndScanRange: downloadAndScanRange, + downloadRange: downloadRange, + scanRange: scanRange, enhanceRange: computeRange(latestHeight: latestEnhancedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight), fetchUTXORange: computeRange(latestHeight: latestUTXOFetchedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight), latestScannedHeight: latestScannedHeight, diff --git a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift index 0ee0384d..19438a35 100644 --- a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift +++ b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift @@ -88,17 +88,10 @@ public enum ZcashSDK { // MARK: Defaults /// Default size of batches of blocks to request from the compact block service. Which was used both for scanning and downloading. - /// consider basing your code assumptions on `DefaultDownloadBatch` and `DefaultScanningBatch` instead. - @available(*, deprecated, message: "this value is being deprecated in favor of `DefaultDownloadBatch` and `DefaultScanningBatch`") public static let DefaultBatchSize = 100 - - /// Default batch size for downloading blocks for the compact block processor. Be careful with this number. This amount of blocks is held in - /// memory at some point of the sync process. - /// This values can't be smaller than `DefaultScanningBatch`. Otherwise bad things will happen. - public static let DefaultDownloadBatch = 100 - - /// Default batch size for scanning blocks for the compact block processor - public static let DefaultScanningBatch = 100 + + /// Default batch size for enhancing transactions for the compact block processor + public static let DefaultEnhanceBatch = 1000 /// Default amount of time, in in seconds, to poll for new blocks. Typically, this should be about half the average /// block time. diff --git a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift index 3d7e8263..6eed7374 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift @@ -180,6 +180,7 @@ protocol LightWalletService: AnyObject { func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched /// - Throws: `serviceFetchUTXOsFailed` when GRPC call fails. + // sourcery: mockedName="fetchUTXOsSingle" func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream /// - Throws: `serviceFetchUTXOsFailed` when GRPC call fails. diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index dcda5c36..1e505e48 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -366,15 +366,9 @@ enum InternalSyncStatus: Equatable { /// taking other maintenance steps that need to occur after an upgrade. case unprepared - case syncing(_ progress: BlockProgress) - - /// Indicates that this Synchronizer is actively enhancing newly scanned blocks - /// with additional transaction details, fetched from the server. - case enhancing(_ progress: EnhancementProgress) - - /// fetches the transparent balance and stores it locally - case fetching(_ progress: Float) - + /// Indicates that this Synchronizer is actively processing new blocks (consists of fetch, scan and enhance operations) + case syncing(Float) + /// Indicates that this Synchronizer is fully up to date and ready for all wallet functions. /// When set, a UI element may want to turn green. case synced @@ -390,7 +384,7 @@ enum InternalSyncStatus: Equatable { public var isSyncing: Bool { switch self { - case .syncing, .enhancing, .fetching: + case .syncing: return true default: return false @@ -416,8 +410,6 @@ enum InternalSyncStatus: Equatable { switch self { case .unprepared: return "unprepared" case .syncing: return "syncing" - case .enhancing: return "enhancing" - case .fetching: return "fetching" case .synced: return "synced" case .stopped: return "stopped" case .disconnected: return "disconnected" @@ -449,8 +441,6 @@ extension InternalSyncStatus { switch (lhs, rhs) { case (.unprepared, .unprepared): return true case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress - case let (.enhancing(lhsProgress), .enhancing(rhsProgress)): return lhsProgress == rhsProgress - case (.fetching, .fetching): return true case (.synced, .synced): return true case (.stopped, .stopped): return true case (.disconnected, .disconnected): return true @@ -461,15 +451,8 @@ extension InternalSyncStatus { } extension InternalSyncStatus { - init(_ blockProcessorProgress: CompactBlockProgress) { - switch blockProcessorProgress { - case .syncing(let progressReport): - self = .syncing(progressReport) - case .enhance(let enhancingReport): - self = .enhancing(enhancingReport) - case .fetch(let fetchingProgress): - self = .fetching(fetchingProgress) - } + init(_ blockProcessorProgress: Float) { + self = .syncing(blockProcessorProgress) } } @@ -479,11 +462,7 @@ extension InternalSyncStatus { case .unprepared: return .unprepared case .syncing(let progress): - return .syncing(0.9 * progress.progress) - case .enhancing(let progress): - return .syncing(0.9 + 0.08 * progress.progress) - case .fetching(let progress): - return .syncing(0.98 + 0.02 * progress) + return .syncing(progress) case .synced: return .upToDate case .stopped: diff --git a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift index 62f9ef64..fb5c9f9f 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift @@ -94,6 +94,10 @@ enum Dependencies { let storage = InternalSyncProgressDiskStorage(storageURL: urls.generalStorageURL, logger: logger) return InternalSyncProgress(alias: alias, storage: storage, logger: logger) } + + container.register(type: ZcashFileManager.self, isSingleton: true) { _ in + FileManager.default + } } static func setupCompactBlockProcessor( @@ -140,7 +144,7 @@ enum Dependencies { let blockScannerConfig = BlockScannerConfig( networkType: config.network.networkType, - scanningBatchSize: config.scanningBatchSize + scanningBatchSize: config.batchSize ) return BlockScannerImpl( diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index aafdff87..b3527e0a 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -160,14 +160,14 @@ public class SDKSynchronizer: Synchronizer { case .unprepared: throw ZcashError.synchronizerNotPrepared - case .syncing, .enhancing, .fetching: + case .syncing: logger.warn("warning: Synchronizer started when already running. Next sync process will be started when the current one stops.") /// This may look strange but `CompactBlockProcessor` has mechanisms which can handle this situation. So we are fine with calling /// it's start here. await blockProcessor.start(retry: retry) case .stopped, .synced, .disconnected, .error: - await updateStatus(.syncing(.nullProgress)) + await updateStatus(.syncing(0)) syncStartDate = Date() await blockProcessor.start(retry: retry) } @@ -200,15 +200,14 @@ public class SDKSynchronizer: Synchronizer { // MARK: Handle CompactBlockProcessor.Flow - // swiftlint:disable:next cyclomatic_complexity private func subscribeToProcessorEvents(_ processor: CompactBlockProcessor) async { let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in switch event { case let .failed(error): await self?.failed(error: error) - case let .finished(height, foundBlocks): - await self?.finished(lastScannedHeight: height, foundBlocks: foundBlocks) + case let .finished(height): + await self?.finished(lastScannedHeight: height) case let .foundTransactions(transactions, range): self?.foundTransactions(transactions: transactions, in: range) @@ -220,17 +219,14 @@ public class SDKSynchronizer: Synchronizer { case let .progressUpdated(progress): await self?.progressUpdated(progress: progress) + case .progressPartialUpdate: + break + case let .storedUTXOs(utxos): self?.storedUTXOs(utxos: utxos) - case .startedEnhancing: - await self?.updateStatus(.enhancing(.zero)) - - case .startedFetching: - await self?.updateStatus(.fetching(0)) - - case .startedSyncing: - await self?.updateStatus(.syncing(.nullProgress)) + case .startedEnhancing, .startedFetching, .startedSyncing: + break case .stopped: await self?.updateStatus(.stopped) @@ -247,7 +243,7 @@ public class SDKSynchronizer: Synchronizer { await updateStatus(.error(error)) } - private func finished(lastScannedHeight: BlockHeight, foundBlocks: Bool) async { + private func finished(lastScannedHeight: BlockHeight) async { await latestBlocksDataProvider.updateScannedData() await updateStatus(.synced) @@ -266,7 +262,7 @@ public class SDKSynchronizer: Synchronizer { } } - private func progressUpdated(progress: CompactBlockProgress) async { + private func progressUpdated(progress: Float) async { let newStatus = InternalSyncStatus(progress) await updateStatus(newStatus) } @@ -407,7 +403,7 @@ public class SDKSynchronizer: Synchronizer { } public func latestHeight() async throws -> BlockHeight { - try await blockProcessor.blockDownloaderService.latestBlockHeight() + try await blockProcessor.latestHeight() } public func latestUTXOs(address: String) async throws -> [UnspentTransactionOutputEntity] { @@ -640,8 +636,6 @@ extension InternalSyncStatus { switch (self, otherStatus) { case (.unprepared, .unprepared): return false case (.syncing, .syncing): return false - case (.enhancing, .enhancing): return false - case (.fetching, .fetching): return false case (.synced, .synced): return false case (.stopped, .stopped): return false case (.disconnected, .disconnected): return false diff --git a/Sources/ZcashLightClientKit/Utils/ZcashFileManager.swift b/Sources/ZcashLightClientKit/Utils/ZcashFileManager.swift new file mode 100644 index 00000000..ccaeb4b7 --- /dev/null +++ b/Sources/ZcashLightClientKit/Utils/ZcashFileManager.swift @@ -0,0 +1,16 @@ +// +// ZcashFileManager.swift +// +// +// Created by Lukáš Korba on 23.05.2023. +// + +import Foundation + +protocol ZcashFileManager { + func isReadableFile(atPath path: String) -> Bool + func removeItem(at URL: URL) throws + func isDeletableFile(atPath path: String) -> Bool +} + +extension FileManager: ZcashFileManager { } diff --git a/Tests/AliasDarksideTests/SDKSynchronizerAliasDarksideTests.swift b/Tests/AliasDarksideTests/SDKSynchronizerAliasDarksideTests.swift index 76dd3b3a..3bfa8b96 100644 --- a/Tests/AliasDarksideTests/SDKSynchronizerAliasDarksideTests.swift +++ b/Tests/AliasDarksideTests/SDKSynchronizerAliasDarksideTests.swift @@ -59,7 +59,7 @@ class SDKSynchronizerAliasDarksideTests: ZcashTestCase { endpoint: endpoint ) - try coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName) + try await coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName) coordinators.append(coordinator) } diff --git a/Tests/DarksideTests/AdvancedReOrgTests.swift b/Tests/DarksideTests/AdvancedReOrgTests.swift index 7bb5adf6..c9ffc51c 100644 --- a/Tests/DarksideTests/AdvancedReOrgTests.swift +++ b/Tests/DarksideTests/AdvancedReOrgTests.swift @@ -33,7 +33,7 @@ class AdvancedReOrgTests: ZcashTestCase { walletBirthday: birthday + 50, network: network ) - try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) } override func tearDown() async throws { @@ -453,7 +453,7 @@ class AdvancedReOrgTests: ZcashTestCase { await hookToReOrgNotification() self.expectedReorgHeight = 663196 self.expectedRewindHeight = 663175 - try coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main") + try await coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main") try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore)) try coordinator.applyStaged(blockheight: 663195) sleep(1) @@ -1031,7 +1031,7 @@ class AdvancedReOrgTests: ZcashTestCase { /// 8. sync to latest height /// 9. verify that the balance is equal to the one before the reorg func testReOrgChangesInboundMinedHeight() async throws { - try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) + try await coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) sleep(2) try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgBefore)) sleep(2) @@ -1096,7 +1096,7 @@ class AdvancedReOrgTests: ZcashTestCase { // FIXME [#644]: Test works with lightwalletd v0.4.13 but is broken when using newer lightwalletd. More info is in #644. func testReOrgRemovesIncomingTxForever() async throws { await hookToReOrgNotification() - try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) + try await coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxBefore)) diff --git a/Tests/DarksideTests/BalanceTests.swift b/Tests/DarksideTests/BalanceTests.swift index dd57305d..90fae922 100644 --- a/Tests/DarksideTests/BalanceTests.swift +++ b/Tests/DarksideTests/BalanceTests.swift @@ -30,7 +30,7 @@ class BalanceTests: ZcashTestCase { walletBirthday: birthday, network: network ) - try coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") + try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") } override func tearDown() async throws { diff --git a/Tests/DarksideTests/DarksideSanityCheckTests.swift b/Tests/DarksideTests/DarksideSanityCheckTests.swift index df4e61b2..73071aad 100644 --- a/Tests/DarksideTests/DarksideSanityCheckTests.swift +++ b/Tests/DarksideTests/DarksideSanityCheckTests.swift @@ -33,7 +33,7 @@ class DarksideSanityCheckTests: ZcashTestCase { network: network ) - try self.coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName) try self.coordinator.resetBlocks(dataset: .default) } diff --git a/Tests/DarksideTests/InternalStateConsistencyTests.swift b/Tests/DarksideTests/InternalStateConsistencyTests.swift deleted file mode 100644 index eaf374d1..00000000 --- a/Tests/DarksideTests/InternalStateConsistencyTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// InternalStateConsistencyTests.swift -// DarksideTests -// -// Created by Francisco Gindre on 1/26/23. -// - -import Combine -import XCTest -@testable import TestUtils -@testable import ZcashLightClientKit - -final class InternalStateConsistencyTests: ZcashTestCase { - let sendAmount = Zatoshi(1000) - var birthday: BlockHeight = 663150 - let defaultLatestHeight: BlockHeight = 663175 - var coordinator: TestCoordinator! - var firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - var expectedReorgHeight: BlockHeight = 665188 - var expectedRewindHeight: BlockHeight = 665188 - var reorgExpectation = XCTestExpectation(description: "reorg") - let branchID = "2bb40e60" - let chainName = "main" - let network = DarksideWalletDNetwork() - var sdkSynchronizerInternalSyncStatusHandler: SDKSynchronizerInternalSyncStatusHandler! = SDKSynchronizerInternalSyncStatusHandler() - - override func setUp() async throws { - try await super.setUp() - - // don't use an exact birthday, users never do. - self.coordinator = try await TestCoordinator( - container: mockContainer, - walletBirthday: birthday + 50, - network: network - ) - - try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) - } - - override func tearDown() async throws { - try await super.tearDown() - let coordinator = self.coordinator! - self.coordinator = nil - sdkSynchronizerInternalSyncStatusHandler = nil - - try await coordinator.stop() - try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot) - try? FileManager.default.removeItem(at: coordinator.databases.dataDB) - } - - func testInternalStateIsConsistentWhenMigrating() async throws { - sdkSynchronizerInternalSyncStatusHandler.subscribe( - to: coordinator.synchronizer.stateStream, - expectations: [.stopped: firstSyncExpectation] - ) - - let fullSyncLength = 10000 - try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength) - - sleep(1) - - // apply the height - try coordinator.applyStaged(blockheight: 664150) - - sleep(1) - - try await coordinator.sync( - completion: { _ in - XCTFail("shouldn't have completed") - }, - error: handleError - ) - - let coordinator = self.coordinator! - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - Task(priority: .userInitiated) { - coordinator.synchronizer.stop() - } - } - - await fulfillment(of: [firstSyncExpectation], timeout: 2) - - let isSyncing = await coordinator.synchronizer.status.isSyncing - let status = await coordinator.synchronizer.status - XCTAssertFalse(isSyncing, "SDKSynchronizer shouldn't be syncing") - XCTAssertEqual(status, .stopped) - - let internalSyncState = coordinator.synchronizer.internalSyncProgress - - let latestDownloadHeight = try await internalSyncState.latestDownloadedBlockHeight - let latestScanHeight = try await coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight() - let dbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedDarksideCacheDb()!) - try dbHandle.setUp() - - if latestDownloadHeight > latestScanHeight { - try await coordinator.synchronizer.blockProcessor.migrateCacheDb(dbHandle.readWriteDb) - - let afterMigrationDownloadedHeight = try await internalSyncState.latestDownloadedBlockHeight - - XCTAssertNotEqual(latestDownloadHeight, afterMigrationDownloadedHeight) - XCTAssertEqual(latestScanHeight, afterMigrationDownloadedHeight) - } else { - try await coordinator.synchronizer.blockProcessor.migrateCacheDb(dbHandle.readWriteDb) - - let afterMigrationDownloadedHeight = try await internalSyncState.latestDownloadedBlockHeight - - XCTAssertEqual(latestDownloadHeight, afterMigrationDownloadedHeight) - XCTAssertEqual(latestScanHeight, afterMigrationDownloadedHeight) - } - - XCTAssertFalse(FileManager.default.isReadableFile(atPath: dbHandle.readWriteDb.path)) - - // clear to simulate a clean slate from the FsBlockDb - try await coordinator.synchronizer.blockProcessor.storage.clear() - - // Now let's resume scanning and see how it goes. - let secondSyncAttemptExpectation = XCTestExpectation(description: "second sync attempt") - - do { - try await coordinator.sync( - completion: { _ in - XCTAssertTrue(true) - secondSyncAttemptExpectation.fulfill() - }, - error: { [weak self] error in - secondSyncAttemptExpectation.fulfill() - self?.handleError(error) - } - ) - } catch { - handleError(error) - } - - await fulfillment(of: [secondSyncAttemptExpectation], timeout: 10) - } - - func handleError(_ error: Error?) { - guard let testError = error else { - XCTFail("failed with nil error") - return - } - XCTFail("Failed with error: \(testError)") - } -} diff --git a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift index a0e8e126..20fbc97e 100644 --- a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift +++ b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift @@ -30,7 +30,7 @@ class PendingTransactionUpdatesTest: ZcashTestCase { walletBirthday: birthday, network: network ) - try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") + try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") } override func tearDown() async throws { diff --git a/Tests/DarksideTests/ReOrgTests.swift b/Tests/DarksideTests/ReOrgTests.swift index 6cd8a50a..23e76aed 100644 --- a/Tests/DarksideTests/ReOrgTests.swift +++ b/Tests/DarksideTests/ReOrgTests.swift @@ -50,7 +50,7 @@ class ReOrgTests: ZcashTestCase { network: self.network ) - try self.coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName) try self.coordinator.resetBlocks(dataset: .default) @@ -128,7 +128,7 @@ class ReOrgTests: ZcashTestCase { targetHeight: BlockHeight ) async throws { do { - try coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName) + try await coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName) try coordinator.resetBlocks(dataset: .predefined(dataset: .beforeReOrg)) try coordinator.applyStaged(blockheight: firstLatestHeight) sleep(1) diff --git a/Tests/DarksideTests/RewindRescanTests.swift b/Tests/DarksideTests/RewindRescanTests.swift index fd8336a2..be2c944f 100644 --- a/Tests/DarksideTests/RewindRescanTests.swift +++ b/Tests/DarksideTests/RewindRescanTests.swift @@ -35,7 +35,7 @@ class RewindRescanTests: ZcashTestCase { walletBirthday: birthday, network: network ) - try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") + try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") } override func tearDown() async throws { diff --git a/Tests/DarksideTests/ShieldFundsTests.swift b/Tests/DarksideTests/ShieldFundsTests.swift index a1047788..bac246b4 100644 --- a/Tests/DarksideTests/ShieldFundsTests.swift +++ b/Tests/DarksideTests/ShieldFundsTests.swift @@ -30,7 +30,7 @@ class ShieldFundsTests: ZcashTestCase { walletBirthday: birthday, network: network ) - try coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName) try coordinator.service.clearAddedUTXOs() } diff --git a/Tests/DarksideTests/SynchronizerDarksideTests.swift b/Tests/DarksideTests/SynchronizerDarksideTests.swift index 17f84e66..10eca140 100644 --- a/Tests/DarksideTests/SynchronizerDarksideTests.swift +++ b/Tests/DarksideTests/SynchronizerDarksideTests.swift @@ -40,7 +40,7 @@ class SynchronizerDarksideTests: ZcashTestCase { network: network ) - try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") + try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main") } override func tearDown() async throws { @@ -195,7 +195,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[0], shieldedBalance: .zero, transparentBalance: .zero, - internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)), + internalSyncStatus: .syncing(0), latestScannedHeight: 663150, latestBlockHeight: 0, latestScannedTime: 1576821833 @@ -204,7 +204,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[0], shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), transparentBalance: .zero, - internalSyncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)), + internalSyncStatus: .syncing(0.9), latestScannedHeight: 663189, latestBlockHeight: 663189, latestScannedTime: 1 @@ -213,84 +213,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[0], shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), transparentBalance: .zero, - internalSyncStatus: .enhancing( - EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: .zero, - internalSyncStatus: .enhancing( - EnhancementProgress( - totalTransactions: 2, - enhancedTransactions: 1, - lastFoundTransaction: ZcashTransaction.Overview( - accountId: 0, - blockTime: 1.0, - expiryHeight: 663206, - fee: Zatoshi(0), - id: 2, - index: 1, - hasChange: false, - memoCount: 1, - minedHeight: 663188, - raw: Data(), - rawID: Data(), - receivedNoteCount: 1, - sentNoteCount: 0, - value: Zatoshi(100000), - isExpiredUmined: false - ), - range: 663150...663189, - newlyMined: true - ) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: .zero, - internalSyncStatus: .enhancing( - EnhancementProgress( - totalTransactions: 2, - enhancedTransactions: 2, - lastFoundTransaction: ZcashTransaction.Overview( - accountId: 0, - blockTime: 1.0, - expiryHeight: 663192, - fee: Zatoshi(0), - id: 1, - index: 1, - hasChange: false, - memoCount: 1, - minedHeight: 663174, - raw: Data(), - rawID: Data(), - receivedNoteCount: 1, - sentNoteCount: 0, - value: Zatoshi(100000), - isExpiredUmined: false - ), - range: 663150...663189, - newlyMined: true - ) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: .zero, - internalSyncStatus: .fetching(0), + internalSyncStatus: .syncing(1.0), latestScannedHeight: 663189, latestBlockHeight: 663189, latestScannedTime: 1 @@ -359,7 +282,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[0], shieldedBalance: .zero, transparentBalance: .zero, - internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)), + internalSyncStatus: .syncing(0), latestScannedHeight: 663150, latestBlockHeight: 0, latestScannedTime: 1576821833.0 @@ -368,7 +291,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[0], shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), transparentBalance: .zero, - internalSyncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)), + internalSyncStatus: .syncing(0.9), latestScannedHeight: 663189, latestBlockHeight: 663189, latestScannedTime: 1 @@ -376,85 +299,8 @@ class SynchronizerDarksideTests: ZcashTestCase { SynchronizerState( syncSessionID: uuids[0], shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .enhancing( - EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .enhancing( - EnhancementProgress( - totalTransactions: 2, - enhancedTransactions: 1, - lastFoundTransaction: ZcashTransaction.Overview( - accountId: 0, - blockTime: 1.0, - expiryHeight: 663206, - fee: nil, - id: 2, - index: 1, - hasChange: false, - memoCount: 1, - minedHeight: 663188, - raw: Data(), - rawID: Data(), - receivedNoteCount: 1, - sentNoteCount: 0, - value: Zatoshi(100000), - isExpiredUmined: false - ), - range: 663150...663189, - newlyMined: true - ) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .enhancing( - EnhancementProgress( - totalTransactions: 2, - enhancedTransactions: 2, - lastFoundTransaction: ZcashTransaction.Overview( - accountId: 0, - blockTime: 1.0, - expiryHeight: 663192, - fee: nil, - id: 1, - index: 1, - hasChange: false, - memoCount: 1, - minedHeight: 663174, - raw: Data(), - rawID: Data(), - receivedNoteCount: 1, - sentNoteCount: 0, - value: Zatoshi(100000), - isExpiredUmined: false - ), - range: 663150...663189, - newlyMined: true - ) - ), - latestScannedHeight: 663189, - latestBlockHeight: 663189, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[0], - shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), - transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .fetching(0), + transparentBalance: .zero, + internalSyncStatus: .syncing(1.0), latestScannedHeight: 663189, latestBlockHeight: 663189, latestScannedTime: 1 @@ -499,7 +345,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[1], shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)), transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)), + internalSyncStatus: .syncing(0), latestScannedHeight: 663189, latestBlockHeight: 663189, latestScannedTime: 1.0 @@ -508,7 +354,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[1], shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)), transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .syncing(BlockProgress(startHeight: 663190, targetHeight: 663200, progressHeight: 663200)), + internalSyncStatus: .syncing(0.9), latestScannedHeight: 663200, latestBlockHeight: 663200, latestScannedTime: 1 @@ -517,18 +363,7 @@ class SynchronizerDarksideTests: ZcashTestCase { syncSessionID: uuids[1], shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)), transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .enhancing( - EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: true) - ), - latestScannedHeight: 663200, - latestBlockHeight: 663200, - latestScannedTime: 1 - ), - SynchronizerState( - syncSessionID: uuids[1], - shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)), - transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)), - internalSyncStatus: .fetching(0), + internalSyncStatus: .syncing(1.0), latestScannedHeight: 663200, latestBlockHeight: 663200, latestScannedTime: 1 diff --git a/Tests/DarksideTests/SynchronizerTests.swift b/Tests/DarksideTests/SynchronizerTests.swift index 990a2cee..30050633 100644 --- a/Tests/DarksideTests/SynchronizerTests.swift +++ b/Tests/DarksideTests/SynchronizerTests.swift @@ -33,7 +33,7 @@ final class SynchronizerTests: ZcashTestCase { walletBirthday: birthday + 50, network: network ) - try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in switch event { @@ -101,8 +101,6 @@ final class SynchronizerTests: ZcashTestCase { let status = await coordinator.synchronizer.status XCTAssertEqual(status, .stopped) - let state = await coordinator.synchronizer.blockProcessor.state - XCTAssertEqual(state, .stopped) } // MARK: Wipe tests @@ -188,8 +186,13 @@ final class SynchronizerTests: ZcashTestCase { try await Task.sleep(nanoseconds: 2_000_000_000) // Just to be sure that blockProcessor is still syncing and that this test does what it should. - let blockProcessorState = await coordinator.synchronizer.blockProcessor.state - XCTAssertEqual(blockProcessorState, .syncing) + let synchronizerState = coordinator.synchronizer.latestState.syncStatus + switch synchronizerState { + case .syncing: + break + default: + XCTFail("Synchornizer should be in syncing state.") + } let wipeFinished = XCTestExpectation(description: "SynchronizerWipeFinished Expectation") /* @@ -223,8 +226,7 @@ final class SynchronizerTests: ZcashTestCase { private func checkThatWipeWorked() async throws { let storage = await self.coordinator.synchronizer.blockProcessor.storage as! FSCompactBlockRepository let fm = FileManager.default - print(coordinator.synchronizer.initializer.dataDbURL.path) - + XCTAssertFalse(fm.fileExists(atPath: coordinator.synchronizer.initializer.dataDbURL.path), "Data DB should be deleted.") XCTAssertTrue(fm.fileExists(atPath: storage.blocksDirectory.path), "FS Cache directory should exist") XCTAssertEqual(try fm.contentsOfDirectory(atPath: storage.blocksDirectory.path), [], "FS Cache directory should be empty") @@ -239,9 +241,6 @@ final class SynchronizerTests: ZcashTestCase { XCTAssertEqual(latestEnhancedHeight, 0, "internalSyncProgress latestEnhancedHeight should be 0") XCTAssertEqual(latestUTXOFetchedHeight, 0, "internalSyncProgress latestUTXOFetchedHeight should be 0") - let blockProcessorState = await coordinator.synchronizer.blockProcessor.state - XCTAssertEqual(blockProcessorState, .stopped, "CompactBlockProcessor state should be stopped") - let status = await coordinator.synchronizer.status XCTAssertEqual(status, .unprepared, "SDKSynchronizer state should be unprepared") } diff --git a/Tests/DarksideTests/Z2TReceiveTests.swift b/Tests/DarksideTests/Z2TReceiveTests.swift index 7019d093..16bf4268 100644 --- a/Tests/DarksideTests/Z2TReceiveTests.swift +++ b/Tests/DarksideTests/Z2TReceiveTests.swift @@ -33,7 +33,7 @@ class Z2TReceiveTests: ZcashTestCase { walletBirthday: birthday, network: network ) - try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) + try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) } override func tearDown() async throws { diff --git a/Tests/NetworkTests/BlockScanTests.swift b/Tests/NetworkTests/BlockScanTests.swift deleted file mode 100644 index 811157ef..00000000 --- a/Tests/NetworkTests/BlockScanTests.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// BlockScanTests.swift -// ZcashLightClientKitTests -// -// Created by Francisco Gindre on 10/17/19. -// Copyright © 2019 Electric Coin Company. All rights reserved. -// - -import Combine -import XCTest -import SQLite -@testable import TestUtils -@testable import ZcashLightClientKit - -class BlockScanTests: ZcashTestCase { - var cancelables: [AnyCancellable] = [] - - var dataDbURL: URL! - var spendParamsURL: URL! - var outputParamsURL: URL! - // swiftlint:disable:next line_length - var saplingExtendedKey = SaplingExtendedFullViewingKey(validatedEncoding: "zxviewtestsapling1qw88ayg8qqqqpqyhg7jnh9mlldejfqwu46pm40ruwstd8znq3v3l4hjf33qcu2a5e36katshcfhcxhzgyfugj2lkhmt40j45cv38rv3frnghzkxcx73k7m7afw9j7ujk7nm4dx5mv02r26umxqgar7v3x390w2h3crqqgjsjly7jy4vtwzrmustm5yudpgcydw7x78awca8wqjvkqj8p8e3ykt7lrgd7xf92fsfqjs5vegfsja4ekzpfh5vtccgvs5747xqm6qflmtqpr8s9u") - - var walletBirthDay = Checkpoint.birthday( - with: 1386000, - network: ZcashNetworkBuilder.network(for: .testnet) - ) - - var rustBackend: ZcashRustBackendWelding! - - var network = ZcashNetworkBuilder.network(for: .testnet) - var blockRepository: BlockRepository! - - let testFileManager = FileManager() - - override func setUp() async throws { - try await super.setUp() - logger = OSLogger(logLevel: .debug) - dataDbURL = try! __dataDbURL() - spendParamsURL = try! __spendParamsURL() - outputParamsURL = try! __outputParamsURL() - - rustBackend = ZcashRustBackend.makeForTests( - dbData: dataDbURL, - fsBlockDbRoot: testTempDirectory, - networkType: network.networkType - ) - - deleteDBs() - - Dependencies.setup( - in: mockContainer, - urls: Initializer.URLs( - fsBlockDbRoot: testTempDirectory, - dataDbURL: dataDbURL, - generalStorageURL: testGeneralStorageDirectory, - spendParamsURL: spendParamsURL, - outputParamsURL: outputParamsURL - ), - alias: .default, - networkType: .testnet, - endpoint: LightWalletEndpointBuilder.default, - loggingPolicy: .default(.debug) - ) - - mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() } - mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in self.rustBackend } - } - - private func deleteDBs() { - try? FileManager.default.removeItem(at: dataDbURL) - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - try super.tearDownWithError() - try? testFileManager.removeItem(at: dataDbURL) - try? testFileManager.removeItem(at: spendParamsURL) - try? testFileManager.removeItem(at: outputParamsURL) - cancelables = [] - blockRepository = nil - testTempDirectory = nil - } - - func testSingleDownloadAndScan() async throws { - _ = try await rustBackend.initDataDb(seed: nil) - - let endpoint = LightWalletEndpoint(address: "lightwalletd.testnet.electriccoin.co", port: 9067) - let blockCount = 100 - let range = network.constants.saplingActivationHeight ... network.constants.saplingActivationHeight + blockCount - - let processorConfig = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: dataDbURL, - spendParamsURL: spendParamsURL, - outputParamsURL: outputParamsURL, - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - walletBirthdayProvider: { [weak self] in self?.walletBirthDay.height ?? .zero }, - network: network - ) - - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletServiceFactory(endpoint: endpoint).make() - } - try await mockContainer.resolve(CompactBlockRepository.self).create() - - let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig) - - let repository = BlockSQLDAO(dbProvider: SimpleConnectionProvider.init(path: self.dataDbURL.absoluteString, readonly: true)) - var latestScannedheight = BlockHeight.empty() - - try await compactBlockProcessor.blockDownloaderService.downloadBlockRange(range) - XCTAssertFalse(Task.isCancelled) - try await compactBlockProcessor.blockScanner.scanBlocks(at: range, totalProgressRange: range, didScan: { _ in }) - - latestScannedheight = repository.lastScannedBlockHeight() - XCTAssertEqual(latestScannedheight, range.upperBound) - - await compactBlockProcessor.stop() - } - - func observeBenchmark(_ metrics: SDKMetrics) { - let reports = metrics.popAllBlockReports(flush: true) - - reports.forEach { - print("observed benchmark: \($0)") - } - } - - func testScanValidateDownload() async throws { - let seed = "testreferencealicetestreferencealice" - - let metrics = SDKMetrics() - metrics.enableMetrics() - - guard try await rustBackend.initDataDb(seed: nil) == .success else { - XCTFail("Seed should not be required for this test") - return - } - - let derivationTool = DerivationTool(networkType: .testnet) - let spendingKey = try derivationTool.deriveUnifiedSpendingKey(seed: Array(seed.utf8), accountIndex: 0) - let viewingKey = try derivationTool.deriveUnifiedFullViewingKey(from: spendingKey) - - do { - try await rustBackend.initAccountsTable(ufvks: [viewingKey]) - } catch { - XCTFail("failed to init account table. error: \(error)") - return - } - - try await rustBackend.initBlocksTable( - height: Int32(walletBirthDay.height), - hash: walletBirthDay.hash, - time: walletBirthDay.time, - saplingTree: walletBirthDay.saplingTree - ) - - let processorConfig = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: dataDbURL, - spendParamsURL: spendParamsURL, - outputParamsURL: outputParamsURL, - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 1000, - scanningBatchSize: 1000, - walletBirthdayProvider: { [weak self] in self?.network.constants.saplingActivationHeight ?? .zero }, - network: network - ) - - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() - } - try await mockContainer.resolve(CompactBlockRepository.self).create() - - let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig) - - let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in - switch event { - case .progressUpdated: self?.observeBenchmark(metrics) - default: break - } - } - - await compactBlockProcessor.updateEventClosure(identifier: "tests", closure: eventClosure) - - let range = CompactBlockRange( - uncheckedBounds: (walletBirthDay.height, walletBirthDay.height + 10000) - ) - - do { - let blockDownloader = await compactBlockProcessor.blockDownloader - await blockDownloader.setDownloadLimit(range.upperBound) - try await blockDownloader.setSyncRange(range, batchSize: 100) - await blockDownloader.startDownload(maxBlockBufferSize: 10) - try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: range) - - XCTAssertFalse(Task.isCancelled) - - try await compactBlockProcessor.blockValidator.validate() - XCTAssertFalse(Task.isCancelled) - - try await compactBlockProcessor.blockScanner.scanBlocks(at: range, totalProgressRange: range, didScan: { _ in }) - XCTAssertFalse(Task.isCancelled) - } catch { - if let lwdError = error as? ZcashError { - switch lwdError { - case .serviceBlockStreamFailed: - XCTAssert(true) - default: - XCTFail("LWD Service error found, but should have been a timeLimit reached Error - \(lwdError)") - } - } else { - XCTFail("Error should have been a timeLimit reached Error - \(error)") - } - } - - await compactBlockProcessor.stop() - metrics.disableMetrics() - } -} diff --git a/Tests/NetworkTests/BlockStreamingTest.swift b/Tests/NetworkTests/BlockStreamingTest.swift index 84662d71..299bc73d 100644 --- a/Tests/NetworkTests/BlockStreamingTest.swift +++ b/Tests/NetworkTests/BlockStreamingTest.swift @@ -12,6 +12,13 @@ import XCTest class BlockStreamingTest: ZcashTestCase { let testFileManager = FileManager() var rustBackend: ZcashRustBackendWelding! + var endpoint: LightWalletEndpoint! + var service: LightWalletService! + var storage: FSCompactBlockRepository! + var internalSyncProgress: InternalSyncProgress! + var processorConfig: CompactBlockProcessor.Configuration! + var latestBlockHeight: BlockHeight! + var startHeight: BlockHeight! override func setUp() async throws { try await super.setUp() @@ -37,45 +44,7 @@ class BlockStreamingTest: ZcashTestCase { mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() } mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in self.rustBackend } - } - override func tearDownWithError() throws { - try super.tearDownWithError() - rustBackend = nil - try? FileManager.default.removeItem(at: __dataDbURL()) - testTempDirectory = nil - } - - func testStream() async throws { - let endpoint = LightWalletEndpoint( - address: LightWalletEndpointBuilder.eccTestnet.host, - port: 9067, - secure: true, - singleCallTimeoutInMillis: 1000, - streamingCallTimeoutInMillis: 100000 - ) - let service = LightWalletServiceFactory(endpoint: endpoint).make() - - let latestHeight = try await service.latestBlockHeight() - - let startHeight = latestHeight - 100_000 - var blocks: [ZcashCompactBlock] = [] - let stream = service.blockStream(startHeight: startHeight, endHeight: latestHeight) - - do { - for try await compactBlock in stream { - print("received block \(compactBlock.height)") - blocks.append(compactBlock) - print("progressHeight: \(compactBlock.height)") - print("startHeight: \(startHeight)") - print("targetHeight: \(latestHeight)") - } - } catch { - XCTFail("failed with error: \(error)") - } - } - - func testStreamCancellation() async throws { let endpoint = LightWalletEndpoint( address: LightWalletEndpointBuilder.eccTestnet.host, port: 9067, @@ -85,51 +54,49 @@ class BlockStreamingTest: ZcashTestCase { ) let service = LightWalletServiceFactory(endpoint: endpoint).make() - let latestBlockHeight = try await service.latestBlockHeight() - let startHeight = latestBlockHeight - 100_000 - let processorConfig = CompactBlockProcessor.Configuration.standard( - for: ZcashNetworkBuilder.network(for: .testnet), - walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight - ) - - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletServiceFactory(endpoint: endpoint).make() - } - try await mockContainer.resolve(CompactBlockRepository.self).create() - - let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig) - - let cancelableTask = Task { - do { - let blockDownloader = await compactBlockProcessor.blockDownloader - await blockDownloader.setDownloadLimit(latestBlockHeight) - try await blockDownloader.setSyncRange(startHeight...latestBlockHeight, batchSize: 100) - await blockDownloader.startDownload(maxBlockBufferSize: 10) - try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: startHeight...latestBlockHeight) - } catch { - XCTAssertTrue(Task.isCancelled) - } - } - - cancelableTask.cancel() - await compactBlockProcessor.stop() + latestBlockHeight = try await service.latestBlockHeight() + startHeight = latestBlockHeight - 10_000 } - - func testStreamTimeout() async throws { + + override func tearDownWithError() throws { + try super.tearDownWithError() + rustBackend = nil + try? FileManager.default.removeItem(at: __dataDbURL()) + endpoint = nil + service = nil + storage = nil + internalSyncProgress = nil + processorConfig = nil + } + + private func makeDependencies(timeout: Int64) async throws { let endpoint = LightWalletEndpoint( address: LightWalletEndpointBuilder.eccTestnet.host, port: 9067, secure: true, - singleCallTimeoutInMillis: 1000, - streamingCallTimeoutInMillis: 1000 + singleCallTimeoutInMillis: timeout, + streamingCallTimeoutInMillis: timeout ) - let service = LightWalletServiceFactory(endpoint: endpoint).make() + self.endpoint = endpoint + service = LightWalletServiceFactory(endpoint: endpoint).make() + storage = FSCompactBlockRepository( + fsBlockDbRoot: testTempDirectory, + metadataStore: FSMetadataStore.live( + fsBlockDbRoot: testTempDirectory, + rustBackend: rustBackend, + logger: logger + ), + blockDescriptor: .live, + contentProvider: DirectoryListingProviders.defaultSorted, + logger: logger + ) + try await storage.create() - let latestBlockHeight = try await service.latestBlockHeight() + let internalSyncProgressStorage = InternalSyncProgressMemoryStorage() + try await internalSyncProgressStorage.set(startHeight, for: InternalSyncProgress.Key.latestDownloadedBlockHeight.rawValue) + internalSyncProgress = InternalSyncProgress(alias: .default, storage: internalSyncProgressStorage, logger: logger) - let startHeight = latestBlockHeight - 100_000 - - let processorConfig = CompactBlockProcessor.Configuration.standard( + processorConfig = CompactBlockProcessor.Configuration.standard( for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight ) @@ -137,18 +104,99 @@ class BlockStreamingTest: ZcashTestCase { mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in LightWalletServiceFactory(endpoint: endpoint).make() } - try await mockContainer.resolve(CompactBlockRepository.self).create() - let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig) - - let date = Date() + let transactionRepositoryMock = TransactionRepositoryMock() + transactionRepositoryMock.lastScannedHeightReturnValue = startHeight + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + + let blockDownloader = BlockDownloaderImpl( + service: service, + downloaderService: BlockDownloaderServiceImpl(service: service, storage: storage), + storage: storage, + internalSyncProgress: internalSyncProgress, + metrics: SDKMetrics(), + logger: logger + ) + mockContainer.mock(type: BlockDownloader.self, isSingleton: true) { _ in blockDownloader } + } + + func testStream() async throws { + try await makeDependencies(timeout: 10000) + + var blocks: [ZcashCompactBlock] = [] + let stream = service.blockStream(startHeight: startHeight, endHeight: latestBlockHeight) do { - let blockDownloader = await compactBlockProcessor.blockDownloader - await blockDownloader.setDownloadLimit(latestBlockHeight) - try await blockDownloader.setSyncRange(startHeight...latestBlockHeight, batchSize: 100) - await blockDownloader.startDownload(maxBlockBufferSize: 10) - try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: startHeight...latestBlockHeight) + for try await compactBlock in stream { + blocks.append(compactBlock) + } + } catch { + XCTFail("failed with error: \(error)") + } + + XCTAssertEqual(blocks.count, latestBlockHeight - startHeight + 1) + } + + func testStreamCancellation() async throws { + try await makeDependencies(timeout: 10000) + + let action = DownloadAction(container: mockContainer, configProvider: CompactBlockProcessor.ConfigProvider(config: processorConfig)) + let syncRanges = SyncRanges( + latestBlockHeight: latestBlockHeight, + downloadRange: startHeight...latestBlockHeight, + scanRange: nil, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: startHeight, + latestDownloadedBlockHeight: startHeight + ) + let context = ActionContext(state: .download) + await context.update(syncRanges: syncRanges) + + let expectation = XCTestExpectation() + + let cancelableTask = Task { + do { + _ = try await action.run(with: context, didUpdate: { _ in }) + let lastDownloadedHeight = try await internalSyncProgress.latestDownloadedBlockHeight + // Just to be sure that download was interrupted before download was finished. + XCTAssertLessThan(lastDownloadedHeight, latestBlockHeight) + expectation.fulfill() + } catch { + XCTFail("Downloading failed with error: \(error)") + expectation.fulfill() + } + } + + DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { + cancelableTask.cancel() + } + + await fulfillment(of: [expectation], timeout: 5) + await action.stop() + } + + func testStreamTimeout() async throws { + try await makeDependencies(timeout: 100) + + let action = DownloadAction(container: mockContainer, configProvider: CompactBlockProcessor.ConfigProvider(config: processorConfig)) + let syncRanges = SyncRanges( + latestBlockHeight: latestBlockHeight, + downloadRange: startHeight...latestBlockHeight, + scanRange: nil, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: startHeight, + latestDownloadedBlockHeight: startHeight + ) + let context = ActionContext(state: .download) + await context.update(syncRanges: syncRanges) + + let date = Date() + + do { + _ = try await action.run(with: context, didUpdate: { _ in }) + XCTFail("It is expected that this downloading fails.") } catch { if let lwdError = error as? ZcashError { switch lwdError { @@ -161,12 +209,12 @@ class BlockStreamingTest: ZcashTestCase { XCTFail("Error should have been a timeLimit reached Error") } } - + let now = Date() - + let elapsed = now.timeIntervalSince(date) print("took \(elapsed) seconds") - await compactBlockProcessor.stop() + await action.stop() } } diff --git a/Tests/NetworkTests/CompactBlockProcessorTests.swift b/Tests/NetworkTests/CompactBlockProcessorTests.swift index e7199655..f9987f37 100644 --- a/Tests/NetworkTests/CompactBlockProcessorTests.swift +++ b/Tests/NetworkTests/CompactBlockProcessorTests.swift @@ -30,6 +30,10 @@ class CompactBlockProcessorTests: ZcashTestCase { try await super.setUp() logger = OSLogger(logLevel: .debug) + for key in InternalSyncProgress.Key.allCases { + UserDefaults.standard.set(0, forKey: key.with(.default)) + } + let pathProvider = DefaultResourceProvider(network: network) processorConfig = CompactBlockProcessor.Configuration( alias: .default, @@ -170,7 +174,7 @@ class CompactBlockProcessorTests: ZcashTestCase { let expectedUpdates = expectedBatches( currentHeight: processorConfig.walletBirthday, targetHeight: mockLatestHeight, - batchSize: processorConfig.downloadBatchSize + batchSize: processorConfig.batchSize ) updatedNotificationExpectation.expectedFulfillmentCount = expectedUpdates @@ -189,8 +193,8 @@ class CompactBlockProcessorTests: ZcashTestCase { var expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, - downloadedButUnscannedRange: 1...latestDownloadedHeight, - downloadAndScanRange: latestDownloadedHeight...latestBlockchainHeight, + downloadRange: latestDownloadedHeight...latestBlockchainHeight, + scanRange: latestDownloadedHeight...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, @@ -217,13 +221,13 @@ class CompactBlockProcessorTests: ZcashTestCase { ) // Test mid-range - latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + ZcashSDK.DefaultDownloadBatch) + latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + ZcashSDK.DefaultBatchSize) latestBlockchainHeight = BlockHeight(network.constants.saplingActivationHeight + 1000) expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, - downloadedButUnscannedRange: 1...latestDownloadedHeight, - downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight, + downloadRange: latestDownloadedHeight + 1...latestBlockchainHeight, + scanRange: processorConfig.walletBirthday...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, @@ -256,8 +260,8 @@ class CompactBlockProcessorTests: ZcashTestCase { expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, - downloadedButUnscannedRange: 1...latestDownloadedHeight, - downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight, + downloadRange: latestDownloadedHeight + 1...latestBlockchainHeight, + scanRange: processorConfig.walletBirthday...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, @@ -283,72 +287,6 @@ class CompactBlockProcessorTests: ZcashTestCase { "Failure when testing last range" ) } - - func testShouldClearBlockCacheReturnsNilWhenScannedHeightEqualsDownloadedHeight() { - /* - downloaded but not scanned: -1...-1 - download and scan: 1493120...2255953 - enhance range: 1410000...2255953 - fetchUTXO range: 1410000...2255953 - total progress range: 1493120...2255953 - */ - - let range = SyncRanges( - latestBlockHeight: 2255953, - downloadedButUnscannedRange: -1 ... -1, - downloadAndScanRange: 1493120...2255953, - enhanceRange: 1410000...2255953, - fetchUTXORange: 1410000...2255953, - latestScannedHeight: 1493119, - latestDownloadedBlockHeight: 1493119 - ) - - XCTAssertNil(range.shouldClearBlockCacheAndUpdateInternalState()) - } - - func testShouldClearBlockCacheReturnsAHeightWhenScannedIsGreaterThanDownloaded() { - /* - downloaded but not scanned: -1...-1 - download and scan: 1493120...2255953 - enhance range: 1410000...2255953 - fetchUTXO range: 1410000...2255953 - total progress range: 1493120...2255953 - */ - - let range = SyncRanges( - latestBlockHeight: 2255953, - downloadedButUnscannedRange: -1 ... -1, - downloadAndScanRange: 1493120...2255953, - enhanceRange: 1410000...2255953, - fetchUTXORange: 1410000...2255953, - latestScannedHeight: 1493129, - latestDownloadedBlockHeight: 1493119 - ) - - XCTAssertEqual(range.shouldClearBlockCacheAndUpdateInternalState(), BlockHeight(1493129)) - } - - func testShouldClearBlockCacheReturnsNilWhenScannedIsGreaterThanDownloaded() { - /* - downloaded but not scanned: 1493120...1494120 - download and scan: 1494121...2255953 - enhance range: 1410000...2255953 - fetchUTXO range: 1410000...2255953 - total progress range: 1493120...2255953 - */ - - let range = SyncRanges( - latestBlockHeight: 2255953, - downloadedButUnscannedRange: 1493120...1494120, - downloadAndScanRange: 1494121...2255953, - enhanceRange: 1410000...2255953, - fetchUTXORange: 1410000...2255953, - latestScannedHeight: 1493119, - latestDownloadedBlockHeight: 1494120 - ) - - XCTAssertNil(range.shouldClearBlockCacheAndUpdateInternalState()) - } func testDetermineLowerBoundPastBirthday() async { let errorHeight = 781_906 diff --git a/Tests/OfflineTests/BlockBatchValidationTests.swift b/Tests/OfflineTests/BlockBatchValidationTests.swift deleted file mode 100644 index 3265e45b..00000000 --- a/Tests/OfflineTests/BlockBatchValidationTests.swift +++ /dev/null @@ -1,607 +0,0 @@ -// -// BlockBatchValidationTests.swift -// ZcashLightClientKit-Unit-Tests -// -// Created by Francisco Gindre on 6/17/21. -// - -import XCTest -@testable import TestUtils -@testable import ZcashLightClientKit - -class BlockBatchValidationTests: ZcashTestCase { - let testFileManager = FileManager() - var rustBackend: ZcashRustBackendWelding! - - override func setUpWithError() throws { - try super.setUpWithError() - Dependencies.setup( - in: mockContainer, - urls: Initializer.URLs( - fsBlockDbRoot: testTempDirectory, - dataDbURL: try! __dataDbURL(), - generalStorageURL: testGeneralStorageDirectory, - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL() - ), - alias: .default, - networkType: .testnet, - endpoint: LightWalletEndpointBuilder.default, - loggingPolicy: .default(.debug) - ) - - mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() } - - rustBackend = ZcashRustBackend.makeForTests(fsBlockDbRoot: testTempDirectory, networkType: .testnet) - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - rustBackend = nil - testTempDirectory = nil - } - - func testBranchIdFailure() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - let service = MockLightWalletService( - latestBlockHeight: 1210000, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } - - let storage = FSCompactBlockRepository( - fsBlockDbRoot: testTempDirectory, - metadataStore: FSMetadataStore.live( - fsBlockDbRoot: testTempDirectory, - rustBackend: rustBackend, - logger: logger - ), - blockDescriptor: .live, - contentProvider: DirectoryListingProviders.defaultSorted, - logger: logger - ) - mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage } - - try await storage.create() - - mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in - let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) - return BlockDownloaderServiceImpl(service: service, storage: repository) - } - - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { 1210000 }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - - var info = LightdInfo() - info.blockHeight = 130000 - info.branch = "d34db33f" - info.chainName = "main" - info.buildUser = "test user" - info.consensusBranchID = "d34db33f" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: Int32(0xd34d)) - mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock } - - let compactBlockProcessor = CompactBlockProcessor( - container: mockContainer, - config: config - ) - - do { - try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self)) - XCTAssertFalse(Task.isCancelled) - } catch { - switch error { - case ZcashError.compactBlockProcessorWrongConsensusBranchId: - break - default: - XCTFail("Expected ZcashError.compactBlockProcessorWrongConsensusBranchId but found \(error)") - } - } - } - - func testBranchNetworkMismatchFailure() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - let service = MockLightWalletService( - latestBlockHeight: 1210000, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } - - let storage = FSCompactBlockRepository( - fsBlockDbRoot: testTempDirectory, - metadataStore: FSMetadataStore.live( - fsBlockDbRoot: testTempDirectory, - rustBackend: rustBackend, - logger: logger - ), - blockDescriptor: .live, - contentProvider: DirectoryListingProviders.defaultSorted, - logger: logger - ) - mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage } - - try await storage.create() - - mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in - let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) - return BlockDownloaderServiceImpl(service: service, storage: repository) - } - - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { 1210000 }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - var info = LightdInfo() - info.blockHeight = 130000 - info.branch = "d34db33f" - info.chainName = "test" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock } - - let compactBlockProcessor = CompactBlockProcessor( - container: mockContainer, - config: config - ) - - do { - try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self)) - XCTAssertFalse(Task.isCancelled) - } catch { - switch error { - case ZcashError.compactBlockProcessorNetworkMismatch(.mainnet, .testnet): - break - default: - XCTFail("Expected ZcashError.compactBlockProcessorNetworkMismatch but found \(error)") - } - } - } - - func testBranchNetworkTypeWrongFailure() async throws { - let network = ZcashNetworkBuilder.network(for: .testnet) - let service = MockLightWalletService( - latestBlockHeight: 1210000, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } - - let storage = FSCompactBlockRepository( - fsBlockDbRoot: testTempDirectory, - metadataStore: FSMetadataStore.live( - fsBlockDbRoot: testTempDirectory, - rustBackend: rustBackend, - logger: logger - ), - blockDescriptor: .live, - contentProvider: DirectoryListingProviders.defaultSorted, - logger: logger - ) - mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage } - - try await storage.create() - - mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in - let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) - return BlockDownloaderServiceImpl(service: service, storage: repository) - } - - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { 1210000 }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - var info = LightdInfo() - info.blockHeight = 130000 - info.branch = "d34db33f" - info.chainName = "another" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock } - - let compactBlockProcessor = CompactBlockProcessor( - container: mockContainer, - config: config - ) - - do { - try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self)) - XCTAssertFalse(Task.isCancelled) - } catch { - switch error { - case ZcashError.compactBlockProcessorChainName: - break - default: - XCTFail("Expected ZcashError.compactBlockProcessorChainName but found \(error)") - } - } - } - - func testSaplingActivationHeightMismatch() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - let service = MockLightWalletService( - latestBlockHeight: 1210000, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } - - let storage = FSCompactBlockRepository( - fsBlockDbRoot: testTempDirectory, - metadataStore: FSMetadataStore.live( - fsBlockDbRoot: testTempDirectory, - rustBackend: rustBackend, - logger: logger - ), - blockDescriptor: .live, - contentProvider: DirectoryListingProviders.defaultSorted, - logger: logger - ) - mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage } - - try await storage.create() - - mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in - let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) - return BlockDownloaderServiceImpl(service: service, storage: repository) - } - - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { 1210000 }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - - var info = LightdInfo() - info.blockHeight = 130000 - info.branch = "d34db33f" - info.chainName = "main" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(3434343) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock } - - let compactBlockProcessor = CompactBlockProcessor( - container: mockContainer, - config: config - ) - - do { - try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self)) - XCTAssertFalse(Task.isCancelled) - } catch { - switch error { - case ZcashError.compactBlockProcessorSaplingActivationMismatch( - network.constants.saplingActivationHeight, - BlockHeight(info.saplingActivationHeight) - ): - break - default: - XCTFail("Expected ZcashError.compactBlockProcessorSaplingActivationMismatch but found \(error)") - } - } - } - - func testResultIsWait() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - - let expectedLatestHeight = BlockHeight(1210000) - let service = MockLightWalletService( - latestBlockHeight: expectedLatestHeight, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - let expectedStoredLatestHeight = BlockHeight(1220000) - let expectedResult = CompactBlockProcessor.NextState.wait( - latestHeight: expectedLatestHeight, - latestDownloadHeight: expectedStoredLatestHeight - ) - - let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight) - let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository) - - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { 1210000 }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - - var info = LightdInfo() - info.blockHeight = UInt64(expectedLatestHeight) - info.branch = "d34db33f" - info.chainName = "main" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - - var nextBatch: CompactBlockProcessor.NextState? - do { - nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState( - service: service, - downloaderService: downloaderService, - latestBlocksDataProvider: LatestBlocksDataProviderMock( - latestScannedHeight: expectedStoredLatestHeight, - latestBlockHeight: expectedLatestHeight - ), - config: config, - rustBackend: mockBackend.rustBackendMock, - internalSyncProgress: InternalSyncProgress( - alias: .default, - storage: InternalSyncProgressMemoryStorage(), - logger: logger - ), - alias: .default - ) - XCTAssertFalse(Task.isCancelled) - } catch { - XCTFail("this shouldn't happen: \(error)") - } - - guard nextBatch != nil else { - XCTFail("result should not be nil") - return - } - - XCTAssertTrue( - { - switch (nextBatch, expectedResult) { - case let (.wait(latestHeight, latestDownloadHeight), .wait(expectedLatestHeight, exectedLatestDownloadHeight)): - return latestHeight == expectedLatestHeight && latestDownloadHeight == exectedLatestDownloadHeight - default: - return false - } - }(), - "Expected \(expectedResult) got: \(String(describing: nextBatch))" - ) - } - - func testResultProcessNew() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - let expectedLatestHeight = BlockHeight(1230000) - let service = MockLightWalletService( - latestBlockHeight: expectedLatestHeight, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - let expectedStoredLatestHeight = BlockHeight(1220000) - let walletBirthday = BlockHeight(1210000) - - let ranges = SyncRanges( - latestBlockHeight: expectedLatestHeight, - downloadedButUnscannedRange: nil, - downloadAndScanRange: expectedStoredLatestHeight + 1...expectedLatestHeight, - enhanceRange: walletBirthday...expectedLatestHeight, - fetchUTXORange: walletBirthday...expectedLatestHeight, - latestScannedHeight: expectedStoredLatestHeight, - latestDownloadedBlockHeight: expectedStoredLatestHeight - ) - let expectedResult = CompactBlockProcessor.NextState.processNewBlocks(ranges: ranges) - - let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight) - let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository) - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { walletBirthday }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - - var info = LightdInfo() - info.blockHeight = UInt64(expectedLatestHeight) - info.branch = "d34db33f" - info.chainName = "main" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - - var nextBatch: CompactBlockProcessor.NextState? - do { - nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState( - service: service, - downloaderService: downloaderService, - latestBlocksDataProvider: LatestBlocksDataProviderMock( - latestScannedHeight: expectedStoredLatestHeight, - latestBlockHeight: expectedLatestHeight - ), - config: config, - rustBackend: mockBackend.rustBackendMock, - internalSyncProgress: InternalSyncProgress( - alias: .default, - storage: InternalSyncProgressMemoryStorage(), - logger: logger - ), - alias: .default - ) - XCTAssertFalse(Task.isCancelled) - } catch { - XCTFail("this shouldn't happen: \(error)") - } - - guard nextBatch != nil else { - XCTFail("result should not be nil") - return - } - - XCTAssertTrue( - { - switch (nextBatch, expectedResult) { - case let (.processNewBlocks(ranges), .processNewBlocks(expectedRanges)): - return ranges == expectedRanges - default: - return false - } - }(), - "Expected \(expectedResult) got: \(String(describing: nextBatch))" - ) - } - - func testResultProcessorFinished() async throws { - let network = ZcashNetworkBuilder.network(for: .mainnet) - let expectedLatestHeight = BlockHeight(1230000) - let service = MockLightWalletService( - latestBlockHeight: expectedLatestHeight, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() - ) - let expectedStoredLatestHeight = BlockHeight(1230000) - let walletBirthday = BlockHeight(1210000) - let expectedResult = CompactBlockProcessor.NextState.finishProcessing(height: expectedStoredLatestHeight) - let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight) - let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository) - let config = CompactBlockProcessor.Configuration( - alias: .default, - fsBlockCacheRoot: testTempDirectory, - dataDb: try! __dataDbURL(), - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL(), - saplingParamsSourceURL: SaplingParamsSourceURL.tests, - downloadBatchSize: 100, - retries: 5, - maxBackoffInterval: 10, - rewindDistance: 100, - walletBirthdayProvider: { walletBirthday }, - saplingActivation: network.constants.saplingActivationHeight, - network: network - ) - - let internalSyncProgress = InternalSyncProgress( - alias: .default, - storage: InternalSyncProgressMemoryStorage(), - logger: logger - ) - try await internalSyncProgress.set(expectedStoredLatestHeight, .latestEnhancedHeight) - try await internalSyncProgress.set(expectedStoredLatestHeight, .latestUTXOFetchedHeight) - - var info = LightdInfo() - info.blockHeight = UInt64(expectedLatestHeight) - info.branch = "d34db33f" - info.chainName = "main" - info.buildUser = "test user" - info.consensusBranchID = "d34db4d" - info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) - - service.mockLightDInfo = info - - let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d) - - var nextBatch: CompactBlockProcessor.NextState? - do { - nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState( - service: service, - downloaderService: downloaderService, - latestBlocksDataProvider: LatestBlocksDataProviderMock( - latestScannedHeight: expectedStoredLatestHeight, - latestBlockHeight: expectedLatestHeight - ), - config: config, - rustBackend: mockBackend.rustBackendMock, - internalSyncProgress: internalSyncProgress, - alias: .default - ) - - XCTAssertFalse(Task.isCancelled) - } catch { - XCTFail("this shouldn't happen: \(error)") - } - - guard nextBatch != nil else { - XCTFail("result should not be nil") - return - } - - XCTAssertTrue( - { - switch (nextBatch, expectedResult) { - case let (.finishProcessing(height), .finishProcessing(expectedHeight)): - return height == expectedHeight - default: - return false - } - }(), - "Expected \(expectedResult) got: \(String(describing: nextBatch))" - ) - } -} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ChecksBeforeSyncActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ChecksBeforeSyncActionTests.swift new file mode 100644 index 00000000..d18ee772 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ChecksBeforeSyncActionTests.swift @@ -0,0 +1,157 @@ +// +// ChecksBeforeSyncActionTests.swift +// +// +// Created by Lukáš Korba on 22.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ChecksBeforeSyncActionTests: ZcashTestCase { + var underlyingDownloadRange: CompactBlockRange? + var underlyingScanRange: CompactBlockRange? + var underlyingLatestScannedHeight: BlockHeight? + var underlyingLatestDownloadedBlockHeight: BlockHeight? + + override func setUp() { + super.setUp() + + underlyingDownloadRange = nil + underlyingScanRange = nil + underlyingLatestScannedHeight = nil + underlyingLatestDownloadedBlockHeight = nil + } + + func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_noDownloadNoScanRange() async throws { + let checksBeforeSyncAction = setupAction() + + let syncRanges = setupSyncRanges() + + let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges) + XCTAssertNil(latestScannedHeight, "latestScannedHeight is expected to be nil.") + } + + func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_nothingToClear() async throws { + let checksBeforeSyncAction = setupAction() + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingLatestScannedHeight = BlockHeight(2000) + underlyingLatestDownloadedBlockHeight = BlockHeight(2000) + + let syncRanges = setupSyncRanges() + + let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges) + XCTAssertNil(latestScannedHeight, "latestScannedHeight is expected to be nil.") + } + + func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_somethingToClear() async throws { + let checksBeforeSyncAction = setupAction() + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingLatestScannedHeight = BlockHeight(2000) + underlyingLatestDownloadedBlockHeight = BlockHeight(1000) + + let syncRanges = setupSyncRanges() + + let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges) + XCTAssertNotNil(latestScannedHeight, "latestScannedHeight is not expected to be nil.") + } + + func testChecksBeforeSyncAction_NextAction_ClearStorage() async throws { + let compactBlockRepository = CompactBlockRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + compactBlockRepository.clearClosure = { } + internalSyncProgressStorageMock.setForClosure = { _, _ in } + + let checksBeforeSyncAction = setupAction( + compactBlockRepository, + internalSyncProgressStorageMock + ) + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingLatestScannedHeight = BlockHeight(2000) + underlyingLatestDownloadedBlockHeight = BlockHeight(1000) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await checksBeforeSyncAction.run(with: syncContext) { _ in } + XCTAssertTrue(compactBlockRepository.clearCalled, "storage.clear() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .fetchUTXO, + "nextContext after .checksBeforeSync is expected to be .fetchUTXO but received \(nextState)" + ) + } catch { + XCTFail("testChecksBeforeSyncAction_NextAction_ClearStorage is not expected to fail. \(error)") + } + } + + func testChecksBeforeSyncAction_NextAction_CreateStorage() async throws { + let compactBlockRepository = CompactBlockRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + compactBlockRepository.createClosure = { } + + let checksBeforeSyncAction = setupAction(compactBlockRepository) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await checksBeforeSyncAction.run(with: syncContext) { _ in } + XCTAssertTrue(compactBlockRepository.createCalled, "storage.create() is expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .fetchUTXO, + "nextContext after .checksBeforeSync is expected to be .fetchUTXO but received \(nextState)" + ) + } catch { + XCTFail("testChecksBeforeSyncAction_NextAction_CreateStorage is not expected to fail. \(error)") + } + } + + private func setupAction( + _ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock(), + _ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> ChecksBeforeSyncAction { + mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in + InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock) + } + + mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } + + return ChecksBeforeSyncAction( + container: mockContainer + ) + } + + private func setupSyncRanges() -> SyncRanges { + SyncRanges( + latestBlockHeight: 0, + downloadRange: underlyingDownloadRange, + scanRange: underlyingScanRange, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: underlyingLatestScannedHeight, + latestDownloadedBlockHeight: underlyingLatestDownloadedBlockHeight + ) + } + + private func setupActionContext() async -> ActionContext { + let syncContext: ActionContext = .init(state: .checksBeforeSync) + + await syncContext.update(syncRanges: setupSyncRanges()) + await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) + + return syncContext + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift new file mode 100644 index 00000000..6bbb04b7 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift @@ -0,0 +1,40 @@ +// +// ClearAlreadyScannedBlocksActionTests.swift +// +// +// Created by Lukáš Korba on 22.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ClearAlreadyScannedBlocksActionTests: ZcashTestCase { + func testClearAlreadyScannedBlocksAction_NextAction() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + + compactBlockRepositoryMock.clearUpToClosure = { _ in } + transactionRepositoryMock.lastScannedHeightReturnValue = 1 + + mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + + let clearAlreadyScannedBlocksAction = ClearAlreadyScannedBlocksAction( + container: mockContainer + ) + + do { + let nextContext = try await clearAlreadyScannedBlocksAction.run(with: .init(state: .clearAlreadyScannedBlocks)) { _ in } + XCTAssertTrue(compactBlockRepositoryMock.clearUpToCalled, "storage.clear(upTo:) is expected to be called.") + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .enhance, + "nextContext after .clearAlreadyScannedBlocks is expected to be .enhance but received \(nextState)" + ) + } catch { + XCTFail("testClearAlreadyScannedBlocksAction_NextAction is not expected to fail. \(error)") + } + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift new file mode 100644 index 00000000..e7fc9f6f --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift @@ -0,0 +1,36 @@ +// +// ClearCacheActionTests.swift +// +// +// Created by Lukáš Korba on 22.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ClearCacheActionTests: ZcashTestCase { + func testClearCacheAction_NextAction() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + compactBlockRepositoryMock.clearClosure = { } + + mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } + + let clearCacheAction = ClearCacheAction( + container: mockContainer + ) + + do { + let nextContext = try await clearCacheAction.run(with: .init(state: .clearCache)) { _ in } + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .finished, + "nextContext after .clearCache is expected to be .finished but received \(nextState)" + ) + } catch { + XCTFail("testClearCacheAction_NextAction is not expected to fail. \(error)") + } + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncRangesActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncRangesActionTests.swift new file mode 100644 index 00000000..12d839bf --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncRangesActionTests.swift @@ -0,0 +1,242 @@ +// +// ComputeSyncRangesActionTests.swift +// +// +// Created by Lukáš Korba on 22.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ComputeSyncRangesActionTests: ZcashTestCase { + var underlyingDownloadRange: CompactBlockRange? + var underlyingScanRange: CompactBlockRange? + + override func setUp() { + super.setUp() + + underlyingDownloadRange = nil + underlyingScanRange = nil + } + + func testComputeSyncRangesAction_computeTotalProgressRange_noDownloadNoScanRange() async throws { + let computeSyncRangesAction = setupAction() + + let syncRanges = setupSyncRanges() + + let totalProgressRange = computeSyncRangesAction.computeTotalProgressRange(from: syncRanges) + + XCTAssertTrue( + totalProgressRange == 0...0, + "testComputeSyncRangesAction_computeTotalProgressRange_noDownloadNoScanRange is expected to be 0...0 but received \(totalProgressRange)" + ) + } + + func testComputeSyncRangesAction_computeTotalProgressRange_ValidRange() async throws { + let computeSyncRangesAction = setupAction() + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncRanges = setupSyncRanges() + let totalProgressRange = computeSyncRangesAction.computeTotalProgressRange(from: syncRanges) + let expectedRange = 1000...2000 + + XCTAssertTrue( + totalProgressRange == expectedRange, + "testComputeSyncRangesAction_computeTotalProgressRange_ValidRange is expected to be \(expectedRange) but received \(totalProgressRange)" + ) + } + + func testComputeSyncRangesAction_finishProcessingCase() async throws { + let blockDownloaderServiceMock = BlockDownloaderServiceMock() + let latestBlocksDataProviderMock = LatestBlocksDataProviderMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + let loggerMock = LoggerMock() + + let computeSyncRangesAction = setupDefaultMocksAndReturnAction( + blockDownloaderServiceMock, + latestBlocksDataProviderMock, + internalSyncProgressStorageMock, + loggerMock + ) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in } + + XCTAssertTrue( + blockDownloaderServiceMock.lastDownloadedBlockHeightCalled, + "downloaderService.lastDownloadedBlockHeight() is expected to be called." + ) + XCTAssertTrue( + latestBlocksDataProviderMock.updateScannedDataCalled, + "latestBlocksDataProvider.updateScannedData() is expected to be called." + ) + XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.") + XCTAssertFalse(loggerMock.infoFileFunctionLineCalled, "logger.info() is not expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .finished, + "nextContext after .computeSyncRanges is expected to be .finished but received \(nextState)" + ) + } catch { + XCTFail("testComputeSyncRangesAction_finishProcessingCase is not expected to fail. \(error)") + } + } + + func testComputeSyncRangesAction_checksBeforeSyncCase() async throws { + let blockDownloaderServiceMock = BlockDownloaderServiceMock() + let latestBlocksDataProviderMock = LatestBlocksDataProviderMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + let loggerMock = LoggerMock() + + let computeSyncRangesAction = setupDefaultMocksAndReturnAction( + blockDownloaderServiceMock, + latestBlocksDataProviderMock, + internalSyncProgressStorageMock, + loggerMock + ) + latestBlocksDataProviderMock.underlyingLatestBlockHeight = 10 + + let syncContext = await setupActionContext() + + do { + let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in } + + XCTAssertTrue( + blockDownloaderServiceMock.lastDownloadedBlockHeightCalled, + "downloaderService.lastDownloadedBlockHeight() is expected to be called." + ) + XCTAssertTrue( + latestBlocksDataProviderMock.updateScannedDataCalled, + "latestBlocksDataProvider.updateScannedData() is expected to be called." + ) + XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.") + XCTAssertFalse(loggerMock.infoFileFunctionLineCalled, "logger.info() is not expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .checksBeforeSync, + "nextContext after .computeSyncRanges is expected to be .checksBeforeSync but received \(nextState)" + ) + } catch { + XCTFail("testComputeSyncRangesAction_checksBeforeSyncCase is not expected to fail. \(error)") + } + } + + func testComputeSyncRangesAction_waitCase() async throws { + let blockDownloaderServiceMock = BlockDownloaderServiceMock() + let latestBlocksDataProviderMock = LatestBlocksDataProviderMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + let loggerMock = LoggerMock() + + let computeSyncRangesAction = setupDefaultMocksAndReturnAction( + blockDownloaderServiceMock, + latestBlocksDataProviderMock, + internalSyncProgressStorageMock, + loggerMock + ) + blockDownloaderServiceMock.lastDownloadedBlockHeightReturnValue = 10 + latestBlocksDataProviderMock.underlyingLatestScannedHeight = 10 + internalSyncProgressStorageMock.integerForReturnValue = 10 + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + + let syncContext = await setupActionContext() + + do { + let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in } + + XCTAssertTrue( + blockDownloaderServiceMock.lastDownloadedBlockHeightCalled, + "downloaderService.lastDownloadedBlockHeight() is expected to be called." + ) + XCTAssertTrue( + latestBlocksDataProviderMock.updateScannedDataCalled, + "latestBlocksDataProvider.updateScannedData() is expected to be called." + ) + XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.") + XCTAssertTrue(loggerMock.infoFileFunctionLineCalled, "logger.info() is expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .finished, + "nextContext after .computeSyncRanges is expected to be .finished but received \(nextState)" + ) + } catch { + XCTFail("testComputeSyncRangesAction_waitCase is not expected to fail. \(error)") + } + } + + private func setupSyncRanges() -> SyncRanges { + SyncRanges( + latestBlockHeight: 0, + downloadRange: underlyingDownloadRange, + scanRange: underlyingScanRange, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + } + + private func setupActionContext() async -> ActionContext { + let syncContext: ActionContext = .init(state: .computeSyncRanges) + + await syncContext.update(syncRanges: setupSyncRanges()) + await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) + + return syncContext + } + + private func setupAction( + _ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(), + _ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(), + _ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> ComputeSyncRangesAction { + mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in + InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock) + } + + mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in blockDownloaderServiceMock } + mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in latestBlocksDataProviderMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0 + ) + + return ComputeSyncRangesAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config) + ) + } + + private func setupDefaultMocksAndReturnAction( + _ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(), + _ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(), + _ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> ComputeSyncRangesAction { + blockDownloaderServiceMock.lastDownloadedBlockHeightReturnValue = 1 + latestBlocksDataProviderMock.underlyingLatestBlockHeight = 1 + latestBlocksDataProviderMock.underlyingLatestScannedHeight = 1 + latestBlocksDataProviderMock.updateScannedDataClosure = { } + latestBlocksDataProviderMock.updateBlockDataClosure = { } + internalSyncProgressStorageMock.integerForReturnValue = 1 + internalSyncProgressStorageMock.boolForReturnValue = true + internalSyncProgressStorageMock.setBoolClosure = { _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + return setupAction( + blockDownloaderServiceMock, + latestBlocksDataProviderMock, + internalSyncProgressStorageMock, + loggerMock + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift new file mode 100644 index 00000000..016adbd8 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift @@ -0,0 +1,184 @@ +// +// DownloadActionTests.swift +// +// +// Created by Lukáš Korba on 21.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class DownloadActionTests: ZcashTestCase { + var underlyingDownloadRange: CompactBlockRange? + var underlyingScanRange: CompactBlockRange? + + func testDownloadAction_NextAction() async throws { + let blockDownloaderMock = BlockDownloaderMock() + let transactionRepositoryMock = TransactionRepositoryMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1 + blockDownloaderMock.setSyncRangeBatchSizeClosure = { _, _ in } + blockDownloaderMock.setDownloadLimitClosure = { _ in } + blockDownloaderMock.startDownloadMaxBlockBufferSizeClosure = { _ in } + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInClosure = { _ in } + + let downloadAction = setupAction( + blockDownloaderMock, + transactionRepositoryMock + ) + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await downloadAction.run(with: syncContext) { _ in } + + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is expected to be called.") + XCTAssertTrue(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is expected to be called.") + XCTAssertTrue(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is expected to be called.") + XCTAssertTrue( + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled, + "downloader.waitUntilRequestedBlocksAreDownloaded() is expected to be called." + ) + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validate, + "nextContext after .download is expected to be .validate but received \(nextState)" + ) + } catch { + XCTFail("testDownloadAction_NextAction is not expected to fail. \(error)") + } + } + + func testDownloadAction_NoDownloadAndScanRange() async throws { + let blockDownloaderMock = BlockDownloaderMock() + let transactionRepositoryMock = TransactionRepositoryMock() + + let downloadAction = setupAction( + blockDownloaderMock, + transactionRepositoryMock + ) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await downloadAction.run(with: syncContext) { _ in } + + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertFalse(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is not expected to be called.") + XCTAssertFalse(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is not expected to be called.") + XCTAssertFalse(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is not expected to be called.") + XCTAssertFalse( + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled, + "downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called." + ) + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validate, + "nextContext after .download is expected to be .validate but received \(nextState)" + ) + } catch { + XCTFail("testDownloadAction_NoDownloadAndScanRange is not expected to fail. \(error)") + } + } + + func testDownloadAction_NothingMoreToDownload() async throws { + let blockDownloaderMock = BlockDownloaderMock() + let transactionRepositoryMock = TransactionRepositoryMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 2001 + + let downloadAction = setupAction( + blockDownloaderMock, + transactionRepositoryMock + ) + + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + + do { + let nextContext = try await downloadAction.run(with: syncContext) { _ in } + + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertFalse(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is not expected to be called.") + XCTAssertFalse(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is not expected to be called.") + XCTAssertFalse(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is not expected to be called.") + XCTAssertFalse( + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled, + "downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called." + ) + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validate, + "nextContext after .download is expected to be .validate but received \(nextState)" + ) + } catch { + XCTFail("testDownloadAction_NothingMoreToDownload is not expected to fail. \(error)") + } + } + + func testDownloadAction_DownloadStops() async throws { + let blockDownloaderMock = BlockDownloaderMock() + + blockDownloaderMock.stopDownloadClosure = { } + + let downloadAction = setupAction( + blockDownloaderMock + ) + + await downloadAction.stop() + + XCTAssertTrue(blockDownloaderMock.stopDownloadCalled, "downloader.stopDownload() is expected to be called.") + } + + private func setupActionContext() async -> ActionContext { + let syncContext: ActionContext = .init(state: .download) + + let syncRanges = SyncRanges( + latestBlockHeight: 0, + downloadRange: underlyingDownloadRange, + scanRange: underlyingScanRange, + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + + await syncContext.update(syncRanges: syncRanges) + + return syncContext + } + + private func setupAction( + _ blockDownloaderMock: BlockDownloaderMock = BlockDownloaderMock(), + _ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> DownloadAction { + mockContainer.mock(type: BlockDownloader.self, isSingleton: true) { _ in blockDownloaderMock } + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0 + ) + + return DownloadAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config) + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift new file mode 100644 index 00000000..08847e71 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift @@ -0,0 +1,360 @@ +// +// EnhanceActionTests.swift +// +// +// Created by Lukáš Korba on 19.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class EnhanceActionTests: ZcashTestCase { + var underlyingDownloadRange: CompactBlockRange? + var underlyingScanRange: CompactBlockRange? + var underlyingEnhanceRange: CompactBlockRange? + + override func setUp() { + super.setUp() + + underlyingDownloadRange = nil + underlyingScanRange = nil + underlyingEnhanceRange = nil + } + + func testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange() async throws { + let enhanceAction = setupAction() + + let syncContext = await setupActionContext() + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1) + let nextState = await nextContext.state + + XCTAssertTrue( + nextState == .clearCache, + "testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange is expected to be .clearCache but received \(nextState)" + ) + } + + func testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft() async throws { + let enhanceAction = setupAction() + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 2000) + let nextState = await nextContext.state + + XCTAssertTrue( + nextState == .clearCache, + "testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft is expected to be .clearCache but received \(nextState)" + ) + } + + func testEnhanceAction_decideWhatToDoNext_DownloadExpected() async throws { + let enhanceAction = setupAction() + underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1500) + let nextState = await nextContext.state + + XCTAssertTrue( + nextState == .download, + "testEnhanceAction_decideWhatToDoNext_DownloadExpected is expected to be .download but received \(nextState)" + ) + } + + func testEnhanceAction_NoEnhanceRange() async throws { + let blockEnhancerMock = BlockEnhancerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1 + + let enhanceAction = setupAction( + blockEnhancerMock, + transactionRepositoryMock, + internalSyncProgressStorageMock + ) + + let syncContext = await setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { _ in } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is not expected to be called.") + } catch { + XCTFail("testEnhanceAction_NoEnhanceRange is not expected to fail. \(error)") + } + } + + func testEnhanceAction_1000BlocksConditionNotFulfilled() async throws { + let blockEnhancerMock = BlockEnhancerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1 + internalSyncProgressStorageMock.integerForReturnValue = 1 + + let enhanceAction = setupAction( + blockEnhancerMock, + transactionRepositoryMock, + internalSyncProgressStorageMock + ) + + underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { _ in } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is expected to be called.") + XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") + } catch { + XCTFail("testEnhanceAction_1000BlocksConditionNotFulfilled is not expected to fail. \(error)") + } + } + + func testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions() async throws { + let blockEnhancerMock = BlockEnhancerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1500 + internalSyncProgressStorageMock.integerForReturnValue = 1 + + let transaction = ZcashTransaction.Overview( + accountId: 0, + blockTime: 1.0, + expiryHeight: 663206, + fee: Zatoshi(0), + id: 2, + index: 1, + hasChange: false, + memoCount: 1, + minedHeight: 663188, + raw: Data(), + rawID: Data(), + receivedNoteCount: 1, + sentNoteCount: 0, + value: Zatoshi(100000), + isExpiredUmined: false + ) + + blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in + await didEnhance(EnhancementProgress.zero) + return [transaction] + } + + let enhanceAction = setupAction( + blockEnhancerMock, + transactionRepositoryMock, + internalSyncProgressStorageMock + ) + + underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { event in + guard case let .foundTransactions(transactions, _) = event else { + XCTFail("Event is expected to be .foundTransactions but received \(event)") + return + } + XCTAssertTrue(transactions.count == 1) + guard let receivedTransaction = transactions.first else { + XCTFail("Transaction.first is expected to pass.") + return + } + + XCTAssertEqual(receivedTransaction.expiryHeight, transaction.expiryHeight, "ReceivedTransaction differs from mocked one.") + } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is expected to be called.") + XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") + } catch { + XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions is not expected to fail. \(error)") + } + } + + func testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction() async throws { + let blockEnhancerMock = BlockEnhancerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1500 + internalSyncProgressStorageMock.integerForReturnValue = 1 + + let transaction = ZcashTransaction.Overview( + accountId: 0, + blockTime: 1.0, + expiryHeight: 663206, + fee: Zatoshi(0), + id: 2, + index: 1, + hasChange: false, + memoCount: 1, + minedHeight: 663188, + raw: Data(), + rawID: Data(), + receivedNoteCount: 1, + sentNoteCount: 0, + value: Zatoshi(100000), + isExpiredUmined: false + ) + + blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in + await didEnhance( + EnhancementProgress( + totalTransactions: 0, + enhancedTransactions: 0, + lastFoundTransaction: transaction, + range: 0...0, + newlyMined: true + ) + ) + return nil + } + + let enhanceAction = setupAction( + blockEnhancerMock, + transactionRepositoryMock, + internalSyncProgressStorageMock + ) + + underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) + + let syncContext = await setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { event in + if case .progressPartialUpdate = event { return } + + guard case .minedTransaction(let minedTransaction) = event else { + XCTFail("Event is expected to be .minedTransaction but received \(event)") + return + } + XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.") + } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is expected to be called.") + XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") + } catch { + XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction is not expected to fail. \(error)") + } + } + + func testEnhanceAction_EnhancementOfBlocksCalled_usingSmallRange_minedTransaction() async throws { + let blockEnhancerMock = BlockEnhancerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 200 + internalSyncProgressStorageMock.integerForReturnValue = 1 + + let transaction = ZcashTransaction.Overview( + accountId: 0, + blockTime: 1.0, + expiryHeight: 663206, + fee: Zatoshi(0), + id: 2, + index: 1, + hasChange: false, + memoCount: 1, + minedHeight: 663188, + raw: Data(), + rawID: Data(), + receivedNoteCount: 1, + sentNoteCount: 0, + value: Zatoshi(100000), + isExpiredUmined: false + ) + + blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in + await didEnhance( + EnhancementProgress( + totalTransactions: 0, + enhancedTransactions: 0, + lastFoundTransaction: transaction, + range: 0...0, + newlyMined: true + ) + ) + return nil + } + + let enhanceAction = setupAction( + blockEnhancerMock, + transactionRepositoryMock, + internalSyncProgressStorageMock + ) + + underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (100, 200)) + + let syncContext = await setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { event in + if case .progressPartialUpdate = event { return } + + guard case .minedTransaction(let minedTransaction) = event else { + XCTFail("Event is expected to be .minedTransaction but received \(event)") + return + } + XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.") + } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is expected to be called.") + XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") + } catch { + XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction is not expected to fail. \(error)") + } + } + + private func setupActionContext() async -> ActionContext { + let syncContext: ActionContext = .init(state: .enhance) + + let syncRanges = SyncRanges( + latestBlockHeight: 0, + downloadRange: underlyingDownloadRange, + scanRange: underlyingScanRange, + enhanceRange: underlyingEnhanceRange, + fetchUTXORange: nil, + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + + await syncContext.update(syncRanges: syncRanges) + await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) + + return syncContext + } + + private func setupAction( + _ blockEnhancerMock: BlockEnhancerMock = BlockEnhancerMock(), + _ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(), + _ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> EnhanceAction { + mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in + InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock) + } + + mockContainer.mock(type: BlockEnhancer.self, isSingleton: true) { _ in blockEnhancerMock } + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0 + ) + + return EnhanceAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config) + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift new file mode 100644 index 00000000..18202ce6 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift @@ -0,0 +1,61 @@ +// +// FetchUTXOsActionTests.swift +// +// +// Created by Lukáš Korba on 18.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class FetchUTXOsActionTests: ZcashTestCase { + func testFetchUTXOsAction_NextAction() async throws { + let loggerMock = LoggerMock() + let uTXOFetcherMock = UTXOFetcherMock() + + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + let insertedEntity = UnspentTransactionOutputEntityMock(address: "addr", txid: Data(), index: 0, script: Data(), valueZat: 1, height: 2) + let skippedEntity = UnspentTransactionOutputEntityMock(address: "addr2", txid: Data(), index: 1, script: Data(), valueZat: 2, height: 3) + uTXOFetcherMock.fetchAtDidFetchReturnValue = (inserted: [insertedEntity], skipped: [skippedEntity]) + + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + mockContainer.mock(type: UTXOFetcher.self, isSingleton: true) { _ in uTXOFetcherMock } + + let fetchUTXOsAction = FetchUTXOsAction(container: mockContainer) + + let syncContext: ActionContext = .init(state: .fetchUTXO) + + let syncRanges = SyncRanges( + latestBlockHeight: 0, + downloadRange: nil, + scanRange: nil, + enhanceRange: nil, + fetchUTXORange: CompactBlockRange(uncheckedBounds: (1000, 2000)), + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + + await syncContext.update(syncRanges: syncRanges) + + do { + let nextContext = try await fetchUTXOsAction.run(with: syncContext) { event in + guard case .storedUTXOs(let result) = event else { + XCTFail("testFetchUTXOsAction_NextAction event expected to be .storedUTXOs but received \(event)") + return + } + XCTAssertEqual(result.inserted as! [UnspentTransactionOutputEntityMock], [insertedEntity]) + XCTAssertEqual(result.skipped as! [UnspentTransactionOutputEntityMock], [skippedEntity]) + } + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") + XCTAssertTrue(uTXOFetcherMock.fetchAtDidFetchCalled, "utxoFetcher.fetch() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .handleSaplingParams, + "nextContext after .fetchUTXO is expected to be .handleSaplingParams but received \(nextState)" + ) + } catch { + XCTFail("testFetchUTXOsAction_NextAction is not expected to fail. \(error)") + } + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift new file mode 100644 index 00000000..42ce1550 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift @@ -0,0 +1,299 @@ +// +// MigrateLegacyCacheDBActionTests.swift +// +// +// Created by Lukáš Korba on 23.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class MigrateLegacyCacheDBActionTests: ZcashTestCase { + var underlyingAlias: ZcashSynchronizerAlias? + var underlyingCacheDbURL: URL? + var underlyingFsBlockCacheRoot: URL? + + override func setUp() { + super.setUp() + + underlyingAlias = nil + underlyingCacheDbURL = nil + underlyingFsBlockCacheRoot = nil + } + + func testMigrateLegacyCacheDBAction_noCacheDbURL() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.") + XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validateServer, + "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" + ) + } catch { + XCTFail("testMigrateLegacyCacheDBAction_noCacheDbURL is not expected to fail. \(error)") + } + } + + func testMigrateLegacyCacheDBAction_noFsBlockCacheRoot() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + _ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + XCTFail("testMigrateLegacyCacheDBAction_noFsBlockCacheRoot is expected to fail.") + } catch ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL { + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.") + XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + } catch { + XCTFail(""" + testMigrateLegacyCacheDBAction_noFsBlockCacheRoot is expected to fail with \ + ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL but received \(error) + """) + } + } + + func testMigrateLegacyCacheDBAction_aliasDoesntMatchDefault() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + // any valid URL needed... + underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL + underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL + + underlyingAlias = .custom("any") + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.") + XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validateServer, + "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" + ) + } catch { + XCTFail("testMigrateLegacyCacheDBAction_aliasDoesntMatchDefault is not expected to fail. \(error)") + } + } + + func testMigrateLegacyCacheDBAction_isNotReadableFile() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + // any valid URL needed... + underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL + underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL + + zcashFileManagerMock.isReadableFileAtPathReturnValue = false + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.") + XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validateServer, + "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" + ) + } catch { + XCTFail("testMigrateLegacyCacheDBAction_isNotReadableFile is not expected to fail. \(error)") + } + } + + func testMigrateLegacyCacheDBAction_removeItemFailed() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + // any valid URL needed... + underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL + underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL + + zcashFileManagerMock.isReadableFileAtPathReturnValue = true + zcashFileManagerMock.removeItemAtClosure = { _ in throw "remove failed" } + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + _ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + } catch ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb { + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.") + XCTAssertTrue(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is expected to be called.") + XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.") + } catch { + XCTFail(""" + testMigrateLegacyCacheDBAction_removeItemFailed is expected to fail with \ + ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb but received \(error) + """) + } + } + + func testMigrateLegacyCacheDBAction_nextAction() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let zcashFileManagerMock = ZcashFileManagerMock() + let internalSyncProgressStorageMock = InternalSyncProgressStorageMock() + + // any valid URL needed... + underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL + underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL + + zcashFileManagerMock.isReadableFileAtPathReturnValue = true + zcashFileManagerMock.removeItemAtClosure = { _ in } + compactBlockRepositoryMock.createClosure = { } + transactionRepositoryMock.lastScannedHeightReturnValue = 1 + internalSyncProgressStorageMock.setForClosure = { _, _ in } + + let migrateLegacyCacheDBAction = setupAction( + compactBlockRepositoryMock, + transactionRepositoryMock, + zcashFileManagerMock, + internalSyncProgressStorageMock + ) + + do { + let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.createCalled, "storage.create() is expected to be called.") + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.") + XCTAssertTrue(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is expected to be called.") + XCTAssertTrue(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is expected to be called.") + + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .validateServer, + "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" + ) + } catch { + XCTFail("testMigrateLegacyCacheDBAction_nextAction is not expected to fail. \(error)") + } + } + + private func setupAction( + _ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock(), + _ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(), + _ zcashFileManagerMock: ZcashFileManagerMock = ZcashFileManagerMock(), + _ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> MigrateLegacyCacheDBAction { + mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in + InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock) + } + mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + mockContainer.mock(type: ZcashFileManager.self, isSingleton: true) { _ in zcashFileManagerMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + return MigrateLegacyCacheDBAction( + container: mockContainer, + configProvider: setupConfig() + ) + } + + private func setupConfig() -> CompactBlockProcessor.ConfigProvider { + let defaultConfig = CompactBlockProcessor.Configuration.standard( + for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0 + ) + + let config = CompactBlockProcessor.Configuration( + alias: underlyingAlias ?? defaultConfig.alias, + cacheDbURL: underlyingCacheDbURL ?? defaultConfig.cacheDbURL, + fsBlockCacheRoot: underlyingFsBlockCacheRoot ?? defaultConfig.fsBlockCacheRoot, + dataDb: defaultConfig.dataDb, + spendParamsURL: defaultConfig.spendParamsURL, + outputParamsURL: defaultConfig.outputParamsURL, + saplingParamsSourceURL: defaultConfig.saplingParamsSourceURL, + walletBirthdayProvider: defaultConfig.walletBirthdayProvider, + saplingActivation: defaultConfig.saplingActivation, + network: defaultConfig.network + ) + + return CompactBlockProcessor.ConfigProvider(config: config) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift new file mode 100644 index 00000000..009fcdf6 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift @@ -0,0 +1,38 @@ +// +// SaplingParamsActionTests.swift +// +// +// Created by Lukáš Korba on 18.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class SaplingParamsActionTests: ZcashTestCase { + func testSaplingParamsAction_NextAction() async throws { + let loggerMock = LoggerMock() + let saplingParametersHandlerMock = SaplingParametersHandlerMock() + + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + saplingParametersHandlerMock.handleIfNeededClosure = { } + + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + mockContainer.mock(type: SaplingParametersHandler.self, isSingleton: true) { _ in saplingParametersHandlerMock } + + let saplingParamsActionAction = SaplingParamsAction(container: mockContainer) + + do { + let nextContext = try await saplingParamsActionAction.run(with: .init(state: .handleSaplingParams)) { _ in } + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") + XCTAssertTrue(saplingParametersHandlerMock.handleIfNeededCalled, "saplingParametersHandler.handleIfNeeded() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .download, + "nextContext after .handleSaplingParams is expected to be .download but received \(nextState)" + ) + } catch { + XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)") + } + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift new file mode 100644 index 00000000..6f2812cc --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift @@ -0,0 +1,126 @@ +// +// ScanActionTests.swift +// +// +// Created by Lukáš Korba on 18.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ScanActionTests: ZcashTestCase { + func testScanAction_NextAction() async throws { + let blockScannerMock = BlockScannerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let loggerMock = LoggerMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 1500 + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + blockScannerMock.scanBlocksAtTotalProgressRangeDidScanClosure = { _, _, _ in 2 } + + let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) + let syncContext = await setupActionContext() + + do { + let nextContext = try await scanAction.run(with: syncContext) { event in + guard case .progressPartialUpdate(.syncing(let progress)) = event else { + XCTFail("event is expected to be .progressPartialUpdate(.syncing()) but received \(event)") + return + } + XCTAssertEqual(progress.startHeight, BlockHeight(1000)) + XCTAssertEqual(progress.targetHeight, BlockHeight(2000)) + XCTAssertEqual(progress.progressHeight, BlockHeight(1500)) + } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") + XCTAssertTrue(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .clearAlreadyScannedBlocks, + "nextContext after .scan is expected to be .clearAlreadyScannedBlocks but received \(nextState)" + ) + } catch { + XCTFail("testScanAction_NextAction is not expected to fail. \(error)") + } + } + + func testScanAction_EarlyOutForNoDownloadAndScanRangeSet() async throws { + let blockScannerMock = BlockScannerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let loggerMock = LoggerMock() + + let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) + let syncContext: ActionContext = .init(state: .scan) + + do { + _ = try await scanAction.run(with: syncContext) { _ in } + XCTAssertFalse( + transactionRepositoryMock.lastScannedHeightCalled, + "transactionRepository.lastScannedHeight() is not expected to be called." + ) + XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is not expected to be called.") + XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is not expected to be called.") + } catch { + XCTFail("testScanAction_EarlyOutForNoDownloadAndScanRangeSet is not expected to fail. \(error)") + } + } + + func testScanAction_StartRangeHigherThanEndRange() async throws { + let blockScannerMock = BlockScannerMock() + let transactionRepositoryMock = TransactionRepositoryMock() + let loggerMock = LoggerMock() + + transactionRepositoryMock.lastScannedHeightReturnValue = 2001 + + let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) + let syncContext = await setupActionContext() + + do { + _ = try await scanAction.run(with: syncContext) { _ in } + XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") + XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is not expected to be called.") + XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is not expected to be called.") + } catch { + XCTFail("testScanAction_StartRangeHigherThanEndRange is not expected to fail. \(error)") + } + } + + private func setupAction( + _ blockScannerMock: BlockScannerMock, + _ transactionRepositoryMock: TransactionRepositoryMock, + _ loggerMock: LoggerMock + ) -> ScanAction { + mockContainer.mock(type: BlockScanner.self, isSingleton: true) { _ in blockScannerMock } + mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0 + ) + + return ScanAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config) + ) + } + + private func setupActionContext() async -> ActionContext { + let syncContext: ActionContext = .init(state: .scan) + + let syncRanges = SyncRanges( + latestBlockHeight: 0, + downloadRange: CompactBlockRange(uncheckedBounds: (1000, 2000)), + scanRange: CompactBlockRange(uncheckedBounds: (1000, 2000)), + enhanceRange: nil, + fetchUTXORange: nil, + latestScannedHeight: nil, + latestDownloadedBlockHeight: nil + ) + + await syncContext.update(syncRanges: syncRanges) + await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) + + return syncContext + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ValidateActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateActionTests.swift new file mode 100644 index 00000000..9f3ba023 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateActionTests.swift @@ -0,0 +1,36 @@ +// +// ValidateActionTests.swift +// +// +// Created by Lukáš Korba on 17.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ValidateActionTests: ZcashTestCase { + func testValidateAction_NextAction() async throws { + let blockValidatorMock = BlockValidatorMock() + + blockValidatorMock.validateClosure = { } + + mockContainer.mock(type: BlockValidator.self, isSingleton: true) { _ in blockValidatorMock } + + let validateAction = ValidateAction( + container: mockContainer + ) + + do { + let nextContext = try await validateAction.run(with: .init(state: .validate)) { _ in } + XCTAssertTrue(blockValidatorMock.validateCalled, "validator.validate() is expected to be called.") + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .scan, + "nextContext after .validate is expected to be .scan but received \(nextState)" + ) + } catch { + XCTFail("testValidateAction_NextAction is not expected to fail. \(error)") + } + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift new file mode 100644 index 00000000..831a6d1f --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift @@ -0,0 +1,163 @@ +// +// ValidateServerActionTests.swift +// +// +// Created by Lukáš Korba on 16.05.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ValidateServerActionTests: ZcashTestCase { + var underlyingChainName = "" + var underlyingNetworkType = NetworkType.testnet + var underlyingSaplingActivationHeight: BlockHeight? + var underlyingConsensusBranchID = "" + + override func setUp() { + super.setUp() + + underlyingChainName = "test" + underlyingNetworkType = .testnet + underlyingSaplingActivationHeight = nil + underlyingConsensusBranchID = "c2d6d0b4" + } + + func testValidateServerAction_NextAction() async throws { + let validateServerAction = setupAction() + + do { + let nextContext = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + let nextState = await nextContext.state + XCTAssertTrue( + nextState == .computeSyncRanges, + "nextContext after .validateServer is expected to be .computeSyncRanges but received \(nextState)" + ) + } catch { + XCTFail("testValidateServerAction_NextAction is not expected to fail. \(error)") + } + } + + func testValidateServerAction_ChainNameError() async throws { + underlyingChainName = "invalid" + + let validateServerAction = setupAction() + + do { + _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + XCTFail("testValidateServerAction_ChainNameError is expected to fail.") + } catch ZcashError.compactBlockProcessorChainName(let chainName) { + XCTAssertEqual(chainName, "invalid") + } catch { + XCTFail(""" + testValidateServerAction_ChainNameError is expected to fail but error \(error) doesn't match \ + ZcashError.compactBlockProcessorChainName + """) + } + } + + func testValidateServerAction_NetworkMatchError() async throws { + underlyingNetworkType = .mainnet + + let validateServerAction = setupAction() + + do { + _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + XCTFail("testValidateServerAction_NetworkMatchError is expected to fail.") + } catch let ZcashError.compactBlockProcessorNetworkMismatch(expected, found) { + XCTAssertEqual(expected, .mainnet) + XCTAssertEqual(found, .testnet) + } catch { + XCTFail(""" + testValidateServerAction_NetworkMatchError is expected to fail but error \(error) doesn't match \ + ZcashError.compactBlockProcessorNetworkMismatch + """) + } + } + + func testValidateServerAction_SaplingActivationError() async throws { + underlyingSaplingActivationHeight = 1 + + let validateServerAction = setupAction() + + do { + _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + XCTFail("testValidateServerAction_SaplingActivationError is expected to fail.") + } catch let ZcashError.compactBlockProcessorSaplingActivationMismatch(expected, found) { + XCTAssertEqual(expected, 280_000) + XCTAssertEqual(found, 1) + } catch { + XCTFail(""" + testValidateServerAction_SaplingActivationError is expected to fail but error \(error) doesn't match \ + ZcashError.compactBlockProcessorSaplingActivationMismatch + """) + } + } + + func testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch() async throws { + underlyingConsensusBranchID = "1 1" + + let validateServerAction = setupAction() + + do { + _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + XCTFail("testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch is expected to fail.") + } catch ZcashError.compactBlockProcessorConsensusBranchID { + } catch { + XCTFail(""" + testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch is expected to fail but error \(error) doesn't match \ + ZcashError.compactBlockProcessorConsensusBranchID + """) + } + } + + func testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch() async throws { + underlyingConsensusBranchID = "1" + + let validateServerAction = setupAction() + + do { + _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + XCTFail("testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch is expected to fail.") + } catch let ZcashError.compactBlockProcessorWrongConsensusBranchId(expected, found) { + XCTAssertEqual(expected, -1026109260) + XCTAssertEqual(found, 1) + } catch { + XCTFail(""" + testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch is expected to fail but error \(error) doesn't match \ + ZcashError.compactBlockProcessorWrongConsensusBranchId + """) + } + } + + private func setupAction() -> ValidateServerAction { + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: underlyingNetworkType), walletBirthday: 0 + ) + + let rustBackendMock = ZcashRustBackendWeldingMock( + consensusBranchIdForHeightClosure: { height in + XCTAssertEqual(height, 2, "") + return -1026109260 + } + ) + + let lightWalletdInfoMock = LightWalletdInfoMock() + lightWalletdInfoMock.underlyingConsensusBranchID = underlyingConsensusBranchID + lightWalletdInfoMock.underlyingSaplingActivationHeight = UInt64(underlyingSaplingActivationHeight ?? config.saplingActivation) + lightWalletdInfoMock.underlyingBlockHeight = 2 + lightWalletdInfoMock.underlyingChainName = underlyingChainName + + let serviceMock = LightWalletServiceMock() + serviceMock.getInfoReturnValue = lightWalletdInfoMock + + mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock } + mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in serviceMock } + + return ValidateServerAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config) + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorOfflineTests.swift b/Tests/OfflineTests/CompactBlockProcessorOfflineTests.swift deleted file mode 100644 index 38bd585c..00000000 --- a/Tests/OfflineTests/CompactBlockProcessorOfflineTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// CompactBlockProcessorOfflineTests.swift -// -// -// Created by Michal Fousek on 15.12.2022. -// - -import XCTest -@testable import TestUtils -@testable import ZcashLightClientKit - -class CompactBlockProcessorOfflineTests: ZcashTestCase { - let testFileManager = FileManager() - - override func setUpWithError() throws { - try super.setUpWithError() - - Dependencies.setup( - in: mockContainer, - urls: Initializer.URLs( - fsBlockDbRoot: testTempDirectory, - dataDbURL: try! __dataDbURL(), - generalStorageURL: testGeneralStorageDirectory, - spendParamsURL: try! __spendParamsURL(), - outputParamsURL: try! __outputParamsURL() - ), - alias: .default, - networkType: .testnet, - endpoint: LightWalletEndpointBuilder.default, - loggingPolicy: .default(.debug) - ) - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - } - - func testComputeProcessingRangeForSingleLoop() async throws { - let network = ZcashNetworkBuilder.network(for: .testnet) - let rustBackend = ZcashRustBackend.makeForTests(fsBlockDbRoot: testTempDirectory, networkType: .testnet) - - let processorConfig = CompactBlockProcessor.Configuration.standard( - for: network, - walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight - ) - - let service = MockLightWalletService( - latestBlockHeight: 690000, - service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() - ) - mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } - - let storage = FSCompactBlockRepository( - fsBlockDbRoot: testTempDirectory, - metadataStore: FSMetadataStore.live( - fsBlockDbRoot: testTempDirectory, - rustBackend: rustBackend, - logger: logger - ), - blockDescriptor: .live, - contentProvider: DirectoryListingProviders.defaultSorted, - logger: logger - ) - mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage } - mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() } - - let processor = CompactBlockProcessor( - container: mockContainer, - config: processorConfig - ) - - let fullRange = 0...1000 - - var range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 0, batchSize: 100) - XCTAssertEqual(range, 0...99) - - range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 5, batchSize: 100) - XCTAssertEqual(range, 500...599) - - range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 10, batchSize: 100) - XCTAssertEqual(range, 1000...1000) - } -} diff --git a/Tests/OfflineTests/InternalSyncProgressTests.swift b/Tests/OfflineTests/InternalSyncProgressTests.swift index 358d44a1..7ebf312e 100644 --- a/Tests/OfflineTests/InternalSyncProgressTests.swift +++ b/Tests/OfflineTests/InternalSyncProgressTests.swift @@ -66,8 +66,8 @@ class InternalSyncProgressTests: ZcashTestCase { switch nextState { case let .processNewBlocks(ranges): - XCTAssertEqual(ranges.downloadedButUnscannedRange, 620001...630000) - XCTAssertEqual(ranges.downloadAndScanRange, 630001...640000) + XCTAssertEqual(ranges.downloadRange, 630001...640000) + XCTAssertEqual(ranges.scanRange, 620001...640000) XCTAssertEqual(ranges.enhanceRange, 630001...640000) XCTAssertEqual(ranges.fetchUTXORange, 630001...640000) diff --git a/Tests/OfflineTests/SynchronizerOfflineTests.swift b/Tests/OfflineTests/SynchronizerOfflineTests.swift index f2e3d7ba..c7be3969 100644 --- a/Tests/OfflineTests/SynchronizerOfflineTests.swift +++ b/Tests/OfflineTests/SynchronizerOfflineTests.swift @@ -368,7 +368,7 @@ class SynchronizerOfflineTests: ZcashTestCase { } func testIsNewSessionOnUnpreparedToValidTransition() { - XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(.nullProgress))) + XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0))) } func testIsNotNewSessionOnUnpreparedToStateThatWontSync() { @@ -378,18 +378,16 @@ class SynchronizerOfflineTests: ZcashTestCase { func testIsNotNewSessionOnUnpreparedToInvalidOrUnexpectedTransitions() { XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .synced)) - XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .fetching(0))) - XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .enhancing(.zero))) } func testIsNotNewSyncSessionOnSameSession() { XCTAssertFalse( SessionTicker.live.isNewSyncSession( .syncing( - BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 3) + 0.5 ), .syncing( - BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4) + 0.6 ) ) ) @@ -400,7 +398,7 @@ class SynchronizerOfflineTests: ZcashTestCase { SessionTicker.live.isNewSyncSession( .synced, .syncing( - BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4) + 0.6 ) ) ) @@ -411,7 +409,7 @@ class SynchronizerOfflineTests: ZcashTestCase { SessionTicker.live.isNewSyncSession( .disconnected, .syncing( - BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4) + 0.6 ) ) ) @@ -422,7 +420,7 @@ class SynchronizerOfflineTests: ZcashTestCase { SessionTicker.live.isNewSyncSession( .stopped, .syncing( - BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4) + 0.6 ) ) ) @@ -430,17 +428,16 @@ class SynchronizerOfflineTests: ZcashTestCase { func testInternalSyncStatusesDontDifferWhenOuterStatusIsTheSame() { XCTAssertFalse(InternalSyncStatus.disconnected.isDifferent(from: .disconnected)) - XCTAssertFalse(InternalSyncStatus.fetching(0).isDifferent(from: .fetching(0))) + XCTAssertFalse(InternalSyncStatus.syncing(0).isDifferent(from: .syncing(0))) XCTAssertFalse(InternalSyncStatus.stopped.isDifferent(from: .stopped)) XCTAssertFalse(InternalSyncStatus.synced.isDifferent(from: .synced)) - XCTAssertFalse(InternalSyncStatus.syncing(.nullProgress).isDifferent(from: .syncing(.nullProgress))) XCTAssertFalse(InternalSyncStatus.unprepared.isDifferent(from: .unprepared)) } func testInternalSyncStatusMap_SyncingLowerBound() { let synchronizerState = synchronizerState( for: - InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 0)) + InternalSyncStatus.syncing(0) ) if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.0, data) { @@ -451,7 +448,7 @@ class SynchronizerOfflineTests: ZcashTestCase { func testInternalSyncStatusMap_SyncingInTheMiddle() { let synchronizerState = synchronizerState( for: - InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 50)) + InternalSyncStatus.syncing(0.45) ) if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.45, data) { @@ -462,89 +459,16 @@ class SynchronizerOfflineTests: ZcashTestCase { func testInternalSyncStatusMap_SyncingUpperBound() { let synchronizerState = synchronizerState( for: - InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 100)) + InternalSyncStatus.syncing(0.9) ) if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) { XCTFail("Syncing is expected to be 90% (0.9) but received \(data).") } } - - func testInternalSyncStatusMap_EnhancingLowerBound() { - let synchronizerState = synchronizerState( - for: - InternalSyncStatus.enhancing( - EnhancementProgress( - totalTransactions: 100, - enhancedTransactions: 0, - lastFoundTransaction: nil, - range: CompactBlockRange(uncheckedBounds: (0, 100)), - newlyMined: false - ) - ) - ) - - if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) { - XCTFail("Syncing is expected to be 90% (0.9) but received \(data).") - } - } - - func testInternalSyncStatusMap_EnhancingInTheMiddle() { - let synchronizerState = synchronizerState( - for: - InternalSyncStatus.enhancing( - EnhancementProgress( - totalTransactions: 100, - enhancedTransactions: 50, - lastFoundTransaction: nil, - range: CompactBlockRange(uncheckedBounds: (0, 100)), - newlyMined: false - ) - ) - ) - - if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.94, data) { - XCTFail("Syncing is expected to be 94% (0.94) but received \(data).") - } - } - - func testInternalSyncStatusMap_EnhancingUpperBound() { - let synchronizerState = synchronizerState( - for: - InternalSyncStatus.enhancing( - EnhancementProgress( - totalTransactions: 100, - enhancedTransactions: 100, - lastFoundTransaction: nil, - range: CompactBlockRange(uncheckedBounds: (0, 100)), - newlyMined: false - ) - ) - ) - - if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.98, data) { - XCTFail("Syncing is expected to be 98% (0.98) but received \(data).") - } - } - - func testInternalSyncStatusMap_FetchingLowerBound() { - let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(0)) - - if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.98, data) { - XCTFail("Syncing is expected to be 98% (0.98) but received \(data).") - } - } - - func testInternalSyncStatusMap_FetchingInTheMiddle() { - let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(0.5)) - - if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.99, data) { - XCTFail("Syncing is expected to be 99% (0.99) but received \(data).") - } - } func testInternalSyncStatusMap_FetchingUpperBound() { - let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(1)) + let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1)) if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(1.0, data) { XCTFail("Syncing is expected to be 100% (1.0) but received \(data).") diff --git a/Tests/TestUtils/CompactBlockProcessorEventHandler.swift b/Tests/TestUtils/CompactBlockProcessorEventHandler.swift index f9093a1a..150a20d5 100644 --- a/Tests/TestUtils/CompactBlockProcessorEventHandler.swift +++ b/Tests/TestUtils/CompactBlockProcessorEventHandler.swift @@ -18,6 +18,7 @@ class CompactBlockProcessorEventHandler { case minedTransaction case handleReorg case progressUpdated + case progressPartialUpdate case storedUTXOs case startedEnhancing case startedFetching @@ -63,6 +64,8 @@ extension CompactBlockProcessor.Event { return .stopped case .minedTransaction: return .minedTransaction + case .progressPartialUpdate: + return .progressPartialUpdate } } } diff --git a/Tests/TestUtils/LatestBlocksDataProviderMock.swift b/Tests/TestUtils/LatestBlocksDataProviderMock.swift deleted file mode 100644 index 62b2f590..00000000 --- a/Tests/TestUtils/LatestBlocksDataProviderMock.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// LatestBlocksDataProviderMock.swift -// -// -// Created by Lukáš Korba on 12.04.2023. -// - -import Foundation -@testable import ZcashLightClientKit - -actor LatestBlocksDataProviderMock: LatestBlocksDataProvider { - private(set) var latestScannedHeight: BlockHeight = .zero - private(set) var latestScannedTime: TimeInterval = 0.0 - private(set) var latestBlockHeight: BlockHeight = .zero - private(set) var walletBirthday: BlockHeight = .zero - - init( - latestScannedHeight: BlockHeight = .zero, - latestScannedTime: TimeInterval = 0, - latestBlockHeight: BlockHeight = .zero, - walletBirthday: BlockHeight = .zero - ) { - self.latestScannedHeight = latestScannedHeight - self.latestScannedTime = latestScannedTime - self.latestBlockHeight = latestBlockHeight - self.walletBirthday = walletBirthday - } - - func updateScannedData() async { } - - func updateBlockData() async { } - - func updateWalletBirthday(_ walletBirthday: BlockHeight) async { } - - func updateLatestScannedHeight(_ latestScannedHeight: BlockHeight) async { } - - func updateLatestScannedTime(_ latestScannedTime: TimeInterval) async { } -} diff --git a/Tests/TestUtils/SDKSynchronizerSyncStatusHandler.swift b/Tests/TestUtils/SDKSynchronizerSyncStatusHandler.swift index 6265bf3a..f62a4cfa 100644 --- a/Tests/TestUtils/SDKSynchronizerSyncStatusHandler.swift +++ b/Tests/TestUtils/SDKSynchronizerSyncStatusHandler.swift @@ -39,8 +39,6 @@ extension InternalSyncStatus { switch self { case .unprepared: return .unprepared case .syncing: return .syncing - case .enhancing: return .enhancing - case .fetching: return .fetching case .synced: return .synced case .stopped: return .stopped case .disconnected: return .disconnected diff --git a/Tests/TestUtils/Sourcery/AutoMockable.stencil b/Tests/TestUtils/Sourcery/AutoMockable.stencil index f14be10b..5ec15abe 100644 --- a/Tests/TestUtils/Sourcery/AutoMockable.stencil +++ b/Tests/TestUtils/Sourcery/AutoMockable.stencil @@ -1,5 +1,6 @@ import Combine @testable import ZcashLightClientKit +import Foundation {% macro methodName method%}{%if method|annotated:"mockedName" %}{{ method.annotations.mockedName }}{% else %}{% call swiftifyMethodName method.selectorName %}{% endif %}{% endmacro %} {% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %} @@ -83,7 +84,7 @@ import Combine {% if method.isStatic %}Self.{% endif %}{% call methodName method %}CallsCount += 1 {% call methodReceivedParameters method %} {% if method.returnTypeName.isVoid %} - {% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) + {% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}{% call methodClosureName method %}!({% call methodClosureCallParameters method %}) {% else %} if let closure = {% if method.isStatic %}Self.{% endif %}{% call methodClosureName method %} { return {% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}closure({% call methodClosureCallParameters method %}) diff --git a/Tests/TestUtils/Sourcery/AutoMockable.swift b/Tests/TestUtils/Sourcery/AutoMockable.swift index c1b6e08d..fb189951 100644 --- a/Tests/TestUtils/Sourcery/AutoMockable.swift +++ b/Tests/TestUtils/Sourcery/AutoMockable.swift @@ -12,7 +12,22 @@ @testable import ZcashLightClientKit -extension ZcashRustBackendWelding { } +extension BlockDownloader { } +extension BlockDownloaderService { } +extension BlockEnhancer { } +extension BlockScanner { } +extension BlockValidator { } +extension CompactBlockRepository { } +extension InternalSyncProgressStorage { } +extension LatestBlocksDataProvider { } +extension LightWalletdInfo { } +extension LightWalletService { } +extension Logger { } +extension SaplingParametersHandler { } extension Synchronizer { } +extension TransactionRepository { } +extension UTXOFetcher { } +extension ZcashFileManager { } +extension ZcashRustBackendWelding { } // sourcery:end: diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index a400794e..57aa6068 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -2,10 +2,1071 @@ // DO NOT EDIT import Combine @testable import ZcashLightClientKit +import Foundation // MARK: - AutoMockable protocols +class BlockDownloaderMock: BlockDownloader { + + + init( + ) { + } + + // MARK: - setDownloadLimit + + var setDownloadLimitCallsCount = 0 + var setDownloadLimitCalled: Bool { + return setDownloadLimitCallsCount > 0 + } + var setDownloadLimitReceivedLimit: BlockHeight? + var setDownloadLimitClosure: ((BlockHeight) async -> Void)? + + func setDownloadLimit(_ limit: BlockHeight) async { + setDownloadLimitCallsCount += 1 + setDownloadLimitReceivedLimit = limit + await setDownloadLimitClosure!(limit) + } + + // MARK: - setSyncRange + + var setSyncRangeBatchSizeThrowableError: Error? + var setSyncRangeBatchSizeCallsCount = 0 + var setSyncRangeBatchSizeCalled: Bool { + return setSyncRangeBatchSizeCallsCount > 0 + } + var setSyncRangeBatchSizeReceivedArguments: (range: CompactBlockRange, batchSize: Int)? + var setSyncRangeBatchSizeClosure: ((CompactBlockRange, Int) async throws -> Void)? + + func setSyncRange(_ range: CompactBlockRange, batchSize: Int) async throws { + if let error = setSyncRangeBatchSizeThrowableError { + throw error + } + setSyncRangeBatchSizeCallsCount += 1 + setSyncRangeBatchSizeReceivedArguments = (range: range, batchSize: batchSize) + try await setSyncRangeBatchSizeClosure!(range, batchSize) + } + + // MARK: - startDownload + + var startDownloadMaxBlockBufferSizeCallsCount = 0 + var startDownloadMaxBlockBufferSizeCalled: Bool { + return startDownloadMaxBlockBufferSizeCallsCount > 0 + } + var startDownloadMaxBlockBufferSizeReceivedMaxBlockBufferSize: Int? + var startDownloadMaxBlockBufferSizeClosure: ((Int) async -> Void)? + + func startDownload(maxBlockBufferSize: Int) async { + startDownloadMaxBlockBufferSizeCallsCount += 1 + startDownloadMaxBlockBufferSizeReceivedMaxBlockBufferSize = maxBlockBufferSize + await startDownloadMaxBlockBufferSizeClosure!(maxBlockBufferSize) + } + + // MARK: - stopDownload + + var stopDownloadCallsCount = 0 + var stopDownloadCalled: Bool { + return stopDownloadCallsCount > 0 + } + var stopDownloadClosure: (() async -> Void)? + + func stopDownload() async { + stopDownloadCallsCount += 1 + await stopDownloadClosure!() + } + + // MARK: - waitUntilRequestedBlocksAreDownloaded + + var waitUntilRequestedBlocksAreDownloadedInThrowableError: Error? + var waitUntilRequestedBlocksAreDownloadedInCallsCount = 0 + var waitUntilRequestedBlocksAreDownloadedInCalled: Bool { + return waitUntilRequestedBlocksAreDownloadedInCallsCount > 0 + } + var waitUntilRequestedBlocksAreDownloadedInReceivedRange: CompactBlockRange? + var waitUntilRequestedBlocksAreDownloadedInClosure: ((CompactBlockRange) async throws -> Void)? + + func waitUntilRequestedBlocksAreDownloaded(in range: CompactBlockRange) async throws { + if let error = waitUntilRequestedBlocksAreDownloadedInThrowableError { + throw error + } + waitUntilRequestedBlocksAreDownloadedInCallsCount += 1 + waitUntilRequestedBlocksAreDownloadedInReceivedRange = range + try await waitUntilRequestedBlocksAreDownloadedInClosure!(range) + } + +} +class BlockDownloaderServiceMock: BlockDownloaderService { + + + init( + ) { + } + var storage: CompactBlockRepository { + get { return underlyingStorage } + } + var underlyingStorage: CompactBlockRepository! + + // MARK: - downloadBlockRange + + var downloadBlockRangeThrowableError: Error? + var downloadBlockRangeCallsCount = 0 + var downloadBlockRangeCalled: Bool { + return downloadBlockRangeCallsCount > 0 + } + var downloadBlockRangeReceivedHeightRange: CompactBlockRange? + var downloadBlockRangeClosure: ((CompactBlockRange) async throws -> Void)? + + func downloadBlockRange(_ heightRange: CompactBlockRange) async throws { + if let error = downloadBlockRangeThrowableError { + throw error + } + downloadBlockRangeCallsCount += 1 + downloadBlockRangeReceivedHeightRange = heightRange + try await downloadBlockRangeClosure!(heightRange) + } + + // MARK: - rewind + + var rewindToThrowableError: Error? + var rewindToCallsCount = 0 + var rewindToCalled: Bool { + return rewindToCallsCount > 0 + } + var rewindToReceivedHeight: BlockHeight? + var rewindToClosure: ((BlockHeight) async throws -> Void)? + + func rewind(to height: BlockHeight) async throws { + if let error = rewindToThrowableError { + throw error + } + rewindToCallsCount += 1 + rewindToReceivedHeight = height + try await rewindToClosure!(height) + } + + // MARK: - lastDownloadedBlockHeight + + var lastDownloadedBlockHeightThrowableError: Error? + var lastDownloadedBlockHeightCallsCount = 0 + var lastDownloadedBlockHeightCalled: Bool { + return lastDownloadedBlockHeightCallsCount > 0 + } + var lastDownloadedBlockHeightReturnValue: BlockHeight! + var lastDownloadedBlockHeightClosure: (() async throws -> BlockHeight)? + + func lastDownloadedBlockHeight() async throws -> BlockHeight { + if let error = lastDownloadedBlockHeightThrowableError { + throw error + } + lastDownloadedBlockHeightCallsCount += 1 + if let closure = lastDownloadedBlockHeightClosure { + return try await closure() + } else { + return lastDownloadedBlockHeightReturnValue + } + } + + // MARK: - latestBlockHeight + + var latestBlockHeightThrowableError: Error? + var latestBlockHeightCallsCount = 0 + var latestBlockHeightCalled: Bool { + return latestBlockHeightCallsCount > 0 + } + var latestBlockHeightReturnValue: BlockHeight! + var latestBlockHeightClosure: (() async throws -> BlockHeight)? + + func latestBlockHeight() async throws -> BlockHeight { + if let error = latestBlockHeightThrowableError { + throw error + } + latestBlockHeightCallsCount += 1 + if let closure = latestBlockHeightClosure { + return try await closure() + } else { + return latestBlockHeightReturnValue + } + } + + // MARK: - fetchTransaction + + var fetchTransactionTxIdThrowableError: Error? + var fetchTransactionTxIdCallsCount = 0 + var fetchTransactionTxIdCalled: Bool { + return fetchTransactionTxIdCallsCount > 0 + } + var fetchTransactionTxIdReceivedTxId: Data? + var fetchTransactionTxIdReturnValue: ZcashTransaction.Fetched! + var fetchTransactionTxIdClosure: ((Data) async throws -> ZcashTransaction.Fetched)? + + func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched { + if let error = fetchTransactionTxIdThrowableError { + throw error + } + fetchTransactionTxIdCallsCount += 1 + fetchTransactionTxIdReceivedTxId = txId + if let closure = fetchTransactionTxIdClosure { + return try await closure(txId) + } else { + return fetchTransactionTxIdReturnValue + } + } + + // MARK: - fetchUnspentTransactionOutputs + + var fetchUnspentTransactionOutputsTAddressStartHeightCallsCount = 0 + var fetchUnspentTransactionOutputsTAddressStartHeightCalled: Bool { + return fetchUnspentTransactionOutputsTAddressStartHeightCallsCount > 0 + } + var fetchUnspentTransactionOutputsTAddressStartHeightReceivedArguments: (tAddress: String, startHeight: BlockHeight)? + var fetchUnspentTransactionOutputsTAddressStartHeightReturnValue: AsyncThrowingStream! + var fetchUnspentTransactionOutputsTAddressStartHeightClosure: ((String, BlockHeight) -> AsyncThrowingStream)? + + func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) -> AsyncThrowingStream { + fetchUnspentTransactionOutputsTAddressStartHeightCallsCount += 1 + fetchUnspentTransactionOutputsTAddressStartHeightReceivedArguments = (tAddress: tAddress, startHeight: startHeight) + if let closure = fetchUnspentTransactionOutputsTAddressStartHeightClosure { + return closure(tAddress, startHeight) + } else { + return fetchUnspentTransactionOutputsTAddressStartHeightReturnValue + } + } + + // MARK: - fetchUnspentTransactionOutputs + + var fetchUnspentTransactionOutputsTAddressesStartHeightCallsCount = 0 + var fetchUnspentTransactionOutputsTAddressesStartHeightCalled: Bool { + return fetchUnspentTransactionOutputsTAddressesStartHeightCallsCount > 0 + } + var fetchUnspentTransactionOutputsTAddressesStartHeightReceivedArguments: (tAddresses: [String], startHeight: BlockHeight)? + var fetchUnspentTransactionOutputsTAddressesStartHeightReturnValue: AsyncThrowingStream! + var fetchUnspentTransactionOutputsTAddressesStartHeightClosure: (([String], BlockHeight) -> AsyncThrowingStream)? + + func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight) -> AsyncThrowingStream { + fetchUnspentTransactionOutputsTAddressesStartHeightCallsCount += 1 + fetchUnspentTransactionOutputsTAddressesStartHeightReceivedArguments = (tAddresses: tAddresses, startHeight: startHeight) + if let closure = fetchUnspentTransactionOutputsTAddressesStartHeightClosure { + return closure(tAddresses, startHeight) + } else { + return fetchUnspentTransactionOutputsTAddressesStartHeightReturnValue + } + } + + // MARK: - closeConnection + + var closeConnectionCallsCount = 0 + var closeConnectionCalled: Bool { + return closeConnectionCallsCount > 0 + } + var closeConnectionClosure: (() -> Void)? + + func closeConnection() { + closeConnectionCallsCount += 1 + closeConnectionClosure!() + } + +} +class BlockEnhancerMock: BlockEnhancer { + + + init( + ) { + } + + // MARK: - enhance + + var enhanceAtDidEnhanceThrowableError: Error? + var enhanceAtDidEnhanceCallsCount = 0 + var enhanceAtDidEnhanceCalled: Bool { + return enhanceAtDidEnhanceCallsCount > 0 + } + var enhanceAtDidEnhanceReceivedArguments: (range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void)? + var enhanceAtDidEnhanceReturnValue: [ZcashTransaction.Overview]? + var enhanceAtDidEnhanceClosure: ((CompactBlockRange, @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?)? + + func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? { + if let error = enhanceAtDidEnhanceThrowableError { + throw error + } + enhanceAtDidEnhanceCallsCount += 1 + enhanceAtDidEnhanceReceivedArguments = (range: range, didEnhance: didEnhance) + if let closure = enhanceAtDidEnhanceClosure { + return try await closure(range, didEnhance) + } else { + return enhanceAtDidEnhanceReturnValue + } + } + +} +class BlockScannerMock: BlockScanner { + + + init( + ) { + } + + // MARK: - scanBlocks + + var scanBlocksAtTotalProgressRangeDidScanThrowableError: Error? + var scanBlocksAtTotalProgressRangeDidScanCallsCount = 0 + var scanBlocksAtTotalProgressRangeDidScanCalled: Bool { + return scanBlocksAtTotalProgressRangeDidScanCallsCount > 0 + } + var scanBlocksAtTotalProgressRangeDidScanReceivedArguments: (range: CompactBlockRange, totalProgressRange: CompactBlockRange, didScan: (BlockHeight) async -> Void)? + var scanBlocksAtTotalProgressRangeDidScanReturnValue: BlockHeight! + var scanBlocksAtTotalProgressRangeDidScanClosure: ((CompactBlockRange, CompactBlockRange, @escaping (BlockHeight) async -> Void) async throws -> BlockHeight)? + + func scanBlocks(at range: CompactBlockRange, totalProgressRange: CompactBlockRange, didScan: @escaping (BlockHeight) async -> Void) async throws -> BlockHeight { + if let error = scanBlocksAtTotalProgressRangeDidScanThrowableError { + throw error + } + scanBlocksAtTotalProgressRangeDidScanCallsCount += 1 + scanBlocksAtTotalProgressRangeDidScanReceivedArguments = (range: range, totalProgressRange: totalProgressRange, didScan: didScan) + if let closure = scanBlocksAtTotalProgressRangeDidScanClosure { + return try await closure(range, totalProgressRange, didScan) + } else { + return scanBlocksAtTotalProgressRangeDidScanReturnValue + } + } + +} +class BlockValidatorMock: BlockValidator { + + + init( + ) { + } + + // MARK: - validate + + var validateThrowableError: Error? + var validateCallsCount = 0 + var validateCalled: Bool { + return validateCallsCount > 0 + } + var validateClosure: (() async throws -> Void)? + + func validate() async throws { + if let error = validateThrowableError { + throw error + } + validateCallsCount += 1 + try await validateClosure!() + } + +} +class CompactBlockRepositoryMock: CompactBlockRepository { + + + init( + ) { + } + + // MARK: - create + + var createThrowableError: Error? + var createCallsCount = 0 + var createCalled: Bool { + return createCallsCount > 0 + } + var createClosure: (() async throws -> Void)? + + func create() async throws { + if let error = createThrowableError { + throw error + } + createCallsCount += 1 + try await createClosure!() + } + + // MARK: - latestHeight + + var latestHeightCallsCount = 0 + var latestHeightCalled: Bool { + return latestHeightCallsCount > 0 + } + var latestHeightReturnValue: BlockHeight! + var latestHeightClosure: (() async -> BlockHeight)? + + func latestHeight() async -> BlockHeight { + latestHeightCallsCount += 1 + if let closure = latestHeightClosure { + return await closure() + } else { + return latestHeightReturnValue + } + } + + // MARK: - write + + var writeBlocksThrowableError: Error? + var writeBlocksCallsCount = 0 + var writeBlocksCalled: Bool { + return writeBlocksCallsCount > 0 + } + var writeBlocksReceivedBlocks: [ZcashCompactBlock]? + var writeBlocksClosure: (([ZcashCompactBlock]) async throws -> Void)? + + func write(blocks: [ZcashCompactBlock]) async throws { + if let error = writeBlocksThrowableError { + throw error + } + writeBlocksCallsCount += 1 + writeBlocksReceivedBlocks = blocks + try await writeBlocksClosure!(blocks) + } + + // MARK: - rewind + + var rewindToThrowableError: Error? + var rewindToCallsCount = 0 + var rewindToCalled: Bool { + return rewindToCallsCount > 0 + } + var rewindToReceivedHeight: BlockHeight? + var rewindToClosure: ((BlockHeight) async throws -> Void)? + + func rewind(to height: BlockHeight) async throws { + if let error = rewindToThrowableError { + throw error + } + rewindToCallsCount += 1 + rewindToReceivedHeight = height + try await rewindToClosure!(height) + } + + // MARK: - clear + + var clearUpToThrowableError: Error? + var clearUpToCallsCount = 0 + var clearUpToCalled: Bool { + return clearUpToCallsCount > 0 + } + var clearUpToReceivedHeight: BlockHeight? + var clearUpToClosure: ((BlockHeight) async throws -> Void)? + + func clear(upTo height: BlockHeight) async throws { + if let error = clearUpToThrowableError { + throw error + } + clearUpToCallsCount += 1 + clearUpToReceivedHeight = height + try await clearUpToClosure!(height) + } + + // MARK: - clear + + var clearThrowableError: Error? + var clearCallsCount = 0 + var clearCalled: Bool { + return clearCallsCount > 0 + } + var clearClosure: (() async throws -> Void)? + + func clear() async throws { + if let error = clearThrowableError { + throw error + } + clearCallsCount += 1 + try await clearClosure!() + } + +} +class InternalSyncProgressStorageMock: InternalSyncProgressStorage { + + + init( + ) { + } + + // MARK: - initialize + + var initializeThrowableError: Error? + var initializeCallsCount = 0 + var initializeCalled: Bool { + return initializeCallsCount > 0 + } + var initializeClosure: (() async throws -> Void)? + + func initialize() async throws { + if let error = initializeThrowableError { + throw error + } + initializeCallsCount += 1 + try await initializeClosure!() + } + + // MARK: - bool + + var boolForThrowableError: Error? + var boolForCallsCount = 0 + var boolForCalled: Bool { + return boolForCallsCount > 0 + } + var boolForReceivedKey: String? + var boolForReturnValue: Bool! + var boolForClosure: ((String) async throws -> Bool)? + + func bool(for key: String) async throws -> Bool { + if let error = boolForThrowableError { + throw error + } + boolForCallsCount += 1 + boolForReceivedKey = key + if let closure = boolForClosure { + return try await closure(key) + } else { + return boolForReturnValue + } + } + + // MARK: - integer + + var integerForThrowableError: Error? + var integerForCallsCount = 0 + var integerForCalled: Bool { + return integerForCallsCount > 0 + } + var integerForReceivedKey: String? + var integerForReturnValue: Int! + var integerForClosure: ((String) async throws -> Int)? + + func integer(for key: String) async throws -> Int { + if let error = integerForThrowableError { + throw error + } + integerForCallsCount += 1 + integerForReceivedKey = key + if let closure = integerForClosure { + return try await closure(key) + } else { + return integerForReturnValue + } + } + + // MARK: - set + + var setForThrowableError: Error? + var setForCallsCount = 0 + var setForCalled: Bool { + return setForCallsCount > 0 + } + var setForReceivedArguments: (value: Int, key: String)? + var setForClosure: ((Int, String) async throws -> Void)? + + func set(_ value: Int, for key: String) async throws { + if let error = setForThrowableError { + throw error + } + setForCallsCount += 1 + setForReceivedArguments = (value: value, key: key) + try await setForClosure!(value, key) + } + + // MARK: - set + + var setBoolThrowableError: Error? + var setBoolCallsCount = 0 + var setBoolCalled: Bool { + return setBoolCallsCount > 0 + } + var setBoolReceivedArguments: (value: Bool, key: String)? + var setBoolClosure: ((Bool, String) async throws -> Void)? + + func set(_ value: Bool, for key: String) async throws { + if let error = setBoolThrowableError { + throw error + } + setBoolCallsCount += 1 + setBoolReceivedArguments = (value: value, key: key) + try await setBoolClosure!(value, key) + } + +} +class LatestBlocksDataProviderMock: LatestBlocksDataProvider { + + + init( + ) { + } + var latestScannedHeight: BlockHeight { + get { return underlyingLatestScannedHeight } + } + var underlyingLatestScannedHeight: BlockHeight! + var latestScannedTime: TimeInterval { + get { return underlyingLatestScannedTime } + } + var underlyingLatestScannedTime: TimeInterval! + var latestBlockHeight: BlockHeight { + get { return underlyingLatestBlockHeight } + } + var underlyingLatestBlockHeight: BlockHeight! + var walletBirthday: BlockHeight { + get { return underlyingWalletBirthday } + } + var underlyingWalletBirthday: BlockHeight! + + // MARK: - updateScannedData + + var updateScannedDataCallsCount = 0 + var updateScannedDataCalled: Bool { + return updateScannedDataCallsCount > 0 + } + var updateScannedDataClosure: (() async -> Void)? + + func updateScannedData() async { + updateScannedDataCallsCount += 1 + await updateScannedDataClosure!() + } + + // MARK: - updateBlockData + + var updateBlockDataCallsCount = 0 + var updateBlockDataCalled: Bool { + return updateBlockDataCallsCount > 0 + } + var updateBlockDataClosure: (() async -> Void)? + + func updateBlockData() async { + updateBlockDataCallsCount += 1 + await updateBlockDataClosure!() + } + + // MARK: - updateWalletBirthday + + var updateWalletBirthdayCallsCount = 0 + var updateWalletBirthdayCalled: Bool { + return updateWalletBirthdayCallsCount > 0 + } + var updateWalletBirthdayReceivedWalletBirthday: BlockHeight? + var updateWalletBirthdayClosure: ((BlockHeight) async -> Void)? + + func updateWalletBirthday(_ walletBirthday: BlockHeight) async { + updateWalletBirthdayCallsCount += 1 + updateWalletBirthdayReceivedWalletBirthday = walletBirthday + await updateWalletBirthdayClosure!(walletBirthday) + } + + // MARK: - updateLatestScannedHeight + + var updateLatestScannedHeightCallsCount = 0 + var updateLatestScannedHeightCalled: Bool { + return updateLatestScannedHeightCallsCount > 0 + } + var updateLatestScannedHeightReceivedLatestScannedHeight: BlockHeight? + var updateLatestScannedHeightClosure: ((BlockHeight) async -> Void)? + + func updateLatestScannedHeight(_ latestScannedHeight: BlockHeight) async { + updateLatestScannedHeightCallsCount += 1 + updateLatestScannedHeightReceivedLatestScannedHeight = latestScannedHeight + await updateLatestScannedHeightClosure!(latestScannedHeight) + } + + // MARK: - updateLatestScannedTime + + var updateLatestScannedTimeCallsCount = 0 + var updateLatestScannedTimeCalled: Bool { + return updateLatestScannedTimeCallsCount > 0 + } + var updateLatestScannedTimeReceivedLatestScannedTime: TimeInterval? + var updateLatestScannedTimeClosure: ((TimeInterval) async -> Void)? + + func updateLatestScannedTime(_ latestScannedTime: TimeInterval) async { + updateLatestScannedTimeCallsCount += 1 + updateLatestScannedTimeReceivedLatestScannedTime = latestScannedTime + await updateLatestScannedTimeClosure!(latestScannedTime) + } + +} +class LightWalletServiceMock: LightWalletService { + + + init( + ) { + } + var connectionStateChange: ((_ from: ConnectionState, _ to: ConnectionState) -> Void)? + + // MARK: - getInfo + + var getInfoThrowableError: Error? + var getInfoCallsCount = 0 + var getInfoCalled: Bool { + return getInfoCallsCount > 0 + } + var getInfoReturnValue: LightWalletdInfo! + var getInfoClosure: (() async throws -> LightWalletdInfo)? + + func getInfo() async throws -> LightWalletdInfo { + if let error = getInfoThrowableError { + throw error + } + getInfoCallsCount += 1 + if let closure = getInfoClosure { + return try await closure() + } else { + return getInfoReturnValue + } + } + + // MARK: - latestBlock + + var latestBlockThrowableError: Error? + var latestBlockCallsCount = 0 + var latestBlockCalled: Bool { + return latestBlockCallsCount > 0 + } + var latestBlockReturnValue: BlockID! + var latestBlockClosure: (() async throws -> BlockID)? + + func latestBlock() async throws -> BlockID { + if let error = latestBlockThrowableError { + throw error + } + latestBlockCallsCount += 1 + if let closure = latestBlockClosure { + return try await closure() + } else { + return latestBlockReturnValue + } + } + + // MARK: - latestBlockHeight + + var latestBlockHeightThrowableError: Error? + var latestBlockHeightCallsCount = 0 + var latestBlockHeightCalled: Bool { + return latestBlockHeightCallsCount > 0 + } + var latestBlockHeightReturnValue: BlockHeight! + var latestBlockHeightClosure: (() async throws -> BlockHeight)? + + func latestBlockHeight() async throws -> BlockHeight { + if let error = latestBlockHeightThrowableError { + throw error + } + latestBlockHeightCallsCount += 1 + if let closure = latestBlockHeightClosure { + return try await closure() + } else { + return latestBlockHeightReturnValue + } + } + + // MARK: - blockRange + + var blockRangeCallsCount = 0 + var blockRangeCalled: Bool { + return blockRangeCallsCount > 0 + } + var blockRangeReceivedRange: CompactBlockRange? + var blockRangeReturnValue: AsyncThrowingStream! + var blockRangeClosure: ((CompactBlockRange) -> AsyncThrowingStream)? + + func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream { + blockRangeCallsCount += 1 + blockRangeReceivedRange = range + if let closure = blockRangeClosure { + return closure(range) + } else { + return blockRangeReturnValue + } + } + + // MARK: - submit + + var submitSpendTransactionThrowableError: Error? + var submitSpendTransactionCallsCount = 0 + var submitSpendTransactionCalled: Bool { + return submitSpendTransactionCallsCount > 0 + } + var submitSpendTransactionReceivedSpendTransaction: Data? + var submitSpendTransactionReturnValue: LightWalletServiceResponse! + var submitSpendTransactionClosure: ((Data) async throws -> LightWalletServiceResponse)? + + func submit(spendTransaction: Data) async throws -> LightWalletServiceResponse { + if let error = submitSpendTransactionThrowableError { + throw error + } + submitSpendTransactionCallsCount += 1 + submitSpendTransactionReceivedSpendTransaction = spendTransaction + if let closure = submitSpendTransactionClosure { + return try await closure(spendTransaction) + } else { + return submitSpendTransactionReturnValue + } + } + + // MARK: - fetchTransaction + + var fetchTransactionTxIdThrowableError: Error? + var fetchTransactionTxIdCallsCount = 0 + var fetchTransactionTxIdCalled: Bool { + return fetchTransactionTxIdCallsCount > 0 + } + var fetchTransactionTxIdReceivedTxId: Data? + var fetchTransactionTxIdReturnValue: ZcashTransaction.Fetched! + var fetchTransactionTxIdClosure: ((Data) async throws -> ZcashTransaction.Fetched)? + + func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched { + if let error = fetchTransactionTxIdThrowableError { + throw error + } + fetchTransactionTxIdCallsCount += 1 + fetchTransactionTxIdReceivedTxId = txId + if let closure = fetchTransactionTxIdClosure { + return try await closure(txId) + } else { + return fetchTransactionTxIdReturnValue + } + } + + // MARK: - fetchUTXOs + + var fetchUTXOsSingleCallsCount = 0 + var fetchUTXOsSingleCalled: Bool { + return fetchUTXOsSingleCallsCount > 0 + } + var fetchUTXOsSingleReceivedArguments: (tAddress: String, height: BlockHeight)? + var fetchUTXOsSingleReturnValue: AsyncThrowingStream! + var fetchUTXOsSingleClosure: ((String, BlockHeight) -> AsyncThrowingStream)? + + func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream { + fetchUTXOsSingleCallsCount += 1 + fetchUTXOsSingleReceivedArguments = (tAddress: tAddress, height: height) + if let closure = fetchUTXOsSingleClosure { + return closure(tAddress, height) + } else { + return fetchUTXOsSingleReturnValue + } + } + + // MARK: - fetchUTXOs + + var fetchUTXOsForHeightCallsCount = 0 + var fetchUTXOsForHeightCalled: Bool { + return fetchUTXOsForHeightCallsCount > 0 + } + var fetchUTXOsForHeightReceivedArguments: (tAddresses: [String], height: BlockHeight)? + var fetchUTXOsForHeightReturnValue: AsyncThrowingStream! + var fetchUTXOsForHeightClosure: (([String], BlockHeight) -> AsyncThrowingStream)? + + func fetchUTXOs(for tAddresses: [String], height: BlockHeight) -> AsyncThrowingStream { + fetchUTXOsForHeightCallsCount += 1 + fetchUTXOsForHeightReceivedArguments = (tAddresses: tAddresses, height: height) + if let closure = fetchUTXOsForHeightClosure { + return closure(tAddresses, height) + } else { + return fetchUTXOsForHeightReturnValue + } + } + + // MARK: - blockStream + + var blockStreamStartHeightEndHeightCallsCount = 0 + var blockStreamStartHeightEndHeightCalled: Bool { + return blockStreamStartHeightEndHeightCallsCount > 0 + } + var blockStreamStartHeightEndHeightReceivedArguments: (startHeight: BlockHeight, endHeight: BlockHeight)? + var blockStreamStartHeightEndHeightReturnValue: AsyncThrowingStream! + var blockStreamStartHeightEndHeightClosure: ((BlockHeight, BlockHeight) -> AsyncThrowingStream)? + + func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream { + blockStreamStartHeightEndHeightCallsCount += 1 + blockStreamStartHeightEndHeightReceivedArguments = (startHeight: startHeight, endHeight: endHeight) + if let closure = blockStreamStartHeightEndHeightClosure { + return closure(startHeight, endHeight) + } else { + return blockStreamStartHeightEndHeightReturnValue + } + } + + // MARK: - closeConnection + + var closeConnectionCallsCount = 0 + var closeConnectionCalled: Bool { + return closeConnectionCallsCount > 0 + } + var closeConnectionClosure: (() -> Void)? + + func closeConnection() { + closeConnectionCallsCount += 1 + closeConnectionClosure!() + } + +} +class LightWalletdInfoMock: LightWalletdInfo { + + + init( + ) { + } + var version: String { + get { return underlyingVersion } + } + var underlyingVersion: String! + var vendor: String { + get { return underlyingVendor } + } + var underlyingVendor: String! + var taddrSupport: Bool { + get { return underlyingTaddrSupport } + } + var underlyingTaddrSupport: Bool! + var chainName: String { + get { return underlyingChainName } + } + var underlyingChainName: String! + var saplingActivationHeight: UInt64 { + get { return underlyingSaplingActivationHeight } + } + var underlyingSaplingActivationHeight: UInt64! + var consensusBranchID: String { + get { return underlyingConsensusBranchID } + } + var underlyingConsensusBranchID: String! + var blockHeight: UInt64 { + get { return underlyingBlockHeight } + } + var underlyingBlockHeight: UInt64! + var gitCommit: String { + get { return underlyingGitCommit } + } + var underlyingGitCommit: String! + var branch: String { + get { return underlyingBranch } + } + var underlyingBranch: String! + var buildDate: String { + get { return underlyingBuildDate } + } + var underlyingBuildDate: String! + var buildUser: String { + get { return underlyingBuildUser } + } + var underlyingBuildUser: String! + var estimatedHeight: UInt64 { + get { return underlyingEstimatedHeight } + } + var underlyingEstimatedHeight: UInt64! + var zcashdBuild: String { + get { return underlyingZcashdBuild } + } + var underlyingZcashdBuild: String! + var zcashdSubversion: String { + get { return underlyingZcashdSubversion } + } + var underlyingZcashdSubversion: String! + +} +class LoggerMock: Logger { + + + init( + ) { + } + + // MARK: - debug + + var debugFileFunctionLineCallsCount = 0 + var debugFileFunctionLineCalled: Bool { + return debugFileFunctionLineCallsCount > 0 + } + var debugFileFunctionLineReceivedArguments: (message: String, file: StaticString, function: StaticString, line: Int)? + var debugFileFunctionLineClosure: ((String, StaticString, StaticString, Int) -> Void)? + + func debug(_ message: String, file: StaticString, function: StaticString, line: Int) { + debugFileFunctionLineCallsCount += 1 + debugFileFunctionLineReceivedArguments = (message: message, file: file, function: function, line: line) + debugFileFunctionLineClosure!(message, file, function, line) + } + + // MARK: - info + + var infoFileFunctionLineCallsCount = 0 + var infoFileFunctionLineCalled: Bool { + return infoFileFunctionLineCallsCount > 0 + } + var infoFileFunctionLineReceivedArguments: (message: String, file: StaticString, function: StaticString, line: Int)? + var infoFileFunctionLineClosure: ((String, StaticString, StaticString, Int) -> Void)? + + func info(_ message: String, file: StaticString, function: StaticString, line: Int) { + infoFileFunctionLineCallsCount += 1 + infoFileFunctionLineReceivedArguments = (message: message, file: file, function: function, line: line) + infoFileFunctionLineClosure!(message, file, function, line) + } + + // MARK: - event + + var eventFileFunctionLineCallsCount = 0 + var eventFileFunctionLineCalled: Bool { + return eventFileFunctionLineCallsCount > 0 + } + var eventFileFunctionLineReceivedArguments: (message: String, file: StaticString, function: StaticString, line: Int)? + var eventFileFunctionLineClosure: ((String, StaticString, StaticString, Int) -> Void)? + + func event(_ message: String, file: StaticString, function: StaticString, line: Int) { + eventFileFunctionLineCallsCount += 1 + eventFileFunctionLineReceivedArguments = (message: message, file: file, function: function, line: line) + eventFileFunctionLineClosure!(message, file, function, line) + } + + // MARK: - warn + + var warnFileFunctionLineCallsCount = 0 + var warnFileFunctionLineCalled: Bool { + return warnFileFunctionLineCallsCount > 0 + } + var warnFileFunctionLineReceivedArguments: (message: String, file: StaticString, function: StaticString, line: Int)? + var warnFileFunctionLineClosure: ((String, StaticString, StaticString, Int) -> Void)? + + func warn(_ message: String, file: StaticString, function: StaticString, line: Int) { + warnFileFunctionLineCallsCount += 1 + warnFileFunctionLineReceivedArguments = (message: message, file: file, function: function, line: line) + warnFileFunctionLineClosure!(message, file, function, line) + } + + // MARK: - error + + var errorFileFunctionLineCallsCount = 0 + var errorFileFunctionLineCalled: Bool { + return errorFileFunctionLineCallsCount > 0 + } + var errorFileFunctionLineReceivedArguments: (message: String, file: StaticString, function: StaticString, line: Int)? + var errorFileFunctionLineClosure: ((String, StaticString, StaticString, Int) -> Void)? + + func error(_ message: String, file: StaticString, function: StaticString, line: Int) { + errorFileFunctionLineCallsCount += 1 + errorFileFunctionLineReceivedArguments = (message: message, file: file, function: function, line: line) + errorFileFunctionLineClosure!(message, file, function, line) + } + +} +class SaplingParametersHandlerMock: SaplingParametersHandler { + + + init( + ) { + } + + // MARK: - handleIfNeeded + + var handleIfNeededThrowableError: Error? + var handleIfNeededCallsCount = 0 + var handleIfNeededCalled: Bool { + return handleIfNeededCallsCount > 0 + } + var handleIfNeededClosure: (() async throws -> Void)? + + func handleIfNeeded() async throws { + if let error = handleIfNeededThrowableError { + throw error + } + handleIfNeededCallsCount += 1 + try await handleIfNeededClosure!() + } + +} class SynchronizerMock: Synchronizer { @@ -93,7 +1154,7 @@ class SynchronizerMock: Synchronizer { } startRetryCallsCount += 1 startRetryReceivedRetry = retry - try await startRetryClosure?(retry) + try await startRetryClosure!(retry) } // MARK: - stop @@ -106,7 +1167,7 @@ class SynchronizerMock: Synchronizer { func stop() { stopCallsCount += 1 - stopClosure?() + stopClosure!() } // MARK: - getSaplingAddress @@ -515,6 +1576,524 @@ class SynchronizerMock: Synchronizer { } } +} +class TransactionRepositoryMock: TransactionRepository { + + + init( + ) { + } + + // MARK: - closeDBConnection + + var closeDBConnectionCallsCount = 0 + var closeDBConnectionCalled: Bool { + return closeDBConnectionCallsCount > 0 + } + var closeDBConnectionClosure: (() -> Void)? + + func closeDBConnection() { + closeDBConnectionCallsCount += 1 + closeDBConnectionClosure!() + } + + // MARK: - countAll + + var countAllThrowableError: Error? + var countAllCallsCount = 0 + var countAllCalled: Bool { + return countAllCallsCount > 0 + } + var countAllReturnValue: Int! + var countAllClosure: (() async throws -> Int)? + + func countAll() async throws -> Int { + if let error = countAllThrowableError { + throw error + } + countAllCallsCount += 1 + if let closure = countAllClosure { + return try await closure() + } else { + return countAllReturnValue + } + } + + // MARK: - countUnmined + + var countUnminedThrowableError: Error? + var countUnminedCallsCount = 0 + var countUnminedCalled: Bool { + return countUnminedCallsCount > 0 + } + var countUnminedReturnValue: Int! + var countUnminedClosure: (() async throws -> Int)? + + func countUnmined() async throws -> Int { + if let error = countUnminedThrowableError { + throw error + } + countUnminedCallsCount += 1 + if let closure = countUnminedClosure { + return try await closure() + } else { + return countUnminedReturnValue + } + } + + // MARK: - blockForHeight + + var blockForHeightThrowableError: Error? + var blockForHeightCallsCount = 0 + var blockForHeightCalled: Bool { + return blockForHeightCallsCount > 0 + } + var blockForHeightReceivedHeight: BlockHeight? + var blockForHeightReturnValue: Block? + var blockForHeightClosure: ((BlockHeight) async throws -> Block?)? + + func blockForHeight(_ height: BlockHeight) async throws -> Block? { + if let error = blockForHeightThrowableError { + throw error + } + blockForHeightCallsCount += 1 + blockForHeightReceivedHeight = height + if let closure = blockForHeightClosure { + return try await closure(height) + } else { + return blockForHeightReturnValue + } + } + + // MARK: - lastScannedHeight + + var lastScannedHeightThrowableError: Error? + var lastScannedHeightCallsCount = 0 + var lastScannedHeightCalled: Bool { + return lastScannedHeightCallsCount > 0 + } + var lastScannedHeightReturnValue: BlockHeight! + var lastScannedHeightClosure: (() async throws -> BlockHeight)? + + func lastScannedHeight() async throws -> BlockHeight { + if let error = lastScannedHeightThrowableError { + throw error + } + lastScannedHeightCallsCount += 1 + if let closure = lastScannedHeightClosure { + return try await closure() + } else { + return lastScannedHeightReturnValue + } + } + + // MARK: - lastScannedBlock + + var lastScannedBlockThrowableError: Error? + var lastScannedBlockCallsCount = 0 + var lastScannedBlockCalled: Bool { + return lastScannedBlockCallsCount > 0 + } + var lastScannedBlockReturnValue: Block? + var lastScannedBlockClosure: (() async throws -> Block?)? + + func lastScannedBlock() async throws -> Block? { + if let error = lastScannedBlockThrowableError { + throw error + } + lastScannedBlockCallsCount += 1 + if let closure = lastScannedBlockClosure { + return try await closure() + } else { + return lastScannedBlockReturnValue + } + } + + // MARK: - isInitialized + + var isInitializedThrowableError: Error? + var isInitializedCallsCount = 0 + var isInitializedCalled: Bool { + return isInitializedCallsCount > 0 + } + var isInitializedReturnValue: Bool! + var isInitializedClosure: (() async throws -> Bool)? + + func isInitialized() async throws -> Bool { + if let error = isInitializedThrowableError { + throw error + } + isInitializedCallsCount += 1 + if let closure = isInitializedClosure { + return try await closure() + } else { + return isInitializedReturnValue + } + } + + // MARK: - find + + var findIdThrowableError: Error? + var findIdCallsCount = 0 + var findIdCalled: Bool { + return findIdCallsCount > 0 + } + var findIdReceivedId: Int? + var findIdReturnValue: ZcashTransaction.Overview! + var findIdClosure: ((Int) async throws -> ZcashTransaction.Overview)? + + func find(id: Int) async throws -> ZcashTransaction.Overview { + if let error = findIdThrowableError { + throw error + } + findIdCallsCount += 1 + findIdReceivedId = id + if let closure = findIdClosure { + return try await closure(id) + } else { + return findIdReturnValue + } + } + + // MARK: - find + + var findRawIDThrowableError: Error? + var findRawIDCallsCount = 0 + var findRawIDCalled: Bool { + return findRawIDCallsCount > 0 + } + var findRawIDReceivedRawID: Data? + var findRawIDReturnValue: ZcashTransaction.Overview! + var findRawIDClosure: ((Data) async throws -> ZcashTransaction.Overview)? + + func find(rawID: Data) async throws -> ZcashTransaction.Overview { + if let error = findRawIDThrowableError { + throw error + } + findRawIDCallsCount += 1 + findRawIDReceivedRawID = rawID + if let closure = findRawIDClosure { + return try await closure(rawID) + } else { + return findRawIDReturnValue + } + } + + // MARK: - find + + var findOffsetLimitKindThrowableError: Error? + var findOffsetLimitKindCallsCount = 0 + var findOffsetLimitKindCalled: Bool { + return findOffsetLimitKindCallsCount > 0 + } + var findOffsetLimitKindReceivedArguments: (offset: Int, limit: Int, kind: TransactionKind)? + var findOffsetLimitKindReturnValue: [ZcashTransaction.Overview]! + var findOffsetLimitKindClosure: ((Int, Int, TransactionKind) async throws -> [ZcashTransaction.Overview])? + + func find(offset: Int, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview] { + if let error = findOffsetLimitKindThrowableError { + throw error + } + findOffsetLimitKindCallsCount += 1 + findOffsetLimitKindReceivedArguments = (offset: offset, limit: limit, kind: kind) + if let closure = findOffsetLimitKindClosure { + return try await closure(offset, limit, kind) + } else { + return findOffsetLimitKindReturnValue + } + } + + // MARK: - find + + var findInLimitKindThrowableError: Error? + var findInLimitKindCallsCount = 0 + var findInLimitKindCalled: Bool { + return findInLimitKindCallsCount > 0 + } + var findInLimitKindReceivedArguments: (range: CompactBlockRange, limit: Int, kind: TransactionKind)? + var findInLimitKindReturnValue: [ZcashTransaction.Overview]! + var findInLimitKindClosure: ((CompactBlockRange, Int, TransactionKind) async throws -> [ZcashTransaction.Overview])? + + func find(in range: CompactBlockRange, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview] { + if let error = findInLimitKindThrowableError { + throw error + } + findInLimitKindCallsCount += 1 + findInLimitKindReceivedArguments = (range: range, limit: limit, kind: kind) + if let closure = findInLimitKindClosure { + return try await closure(range, limit, kind) + } else { + return findInLimitKindReturnValue + } + } + + // MARK: - find + + var findFromLimitKindThrowableError: Error? + var findFromLimitKindCallsCount = 0 + var findFromLimitKindCalled: Bool { + return findFromLimitKindCallsCount > 0 + } + var findFromLimitKindReceivedArguments: (from: ZcashTransaction.Overview, limit: Int, kind: TransactionKind)? + var findFromLimitKindReturnValue: [ZcashTransaction.Overview]! + var findFromLimitKindClosure: ((ZcashTransaction.Overview, Int, TransactionKind) async throws -> [ZcashTransaction.Overview])? + + func find(from: ZcashTransaction.Overview, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview] { + if let error = findFromLimitKindThrowableError { + throw error + } + findFromLimitKindCallsCount += 1 + findFromLimitKindReceivedArguments = (from: from, limit: limit, kind: kind) + if let closure = findFromLimitKindClosure { + return try await closure(from, limit, kind) + } else { + return findFromLimitKindReturnValue + } + } + + // MARK: - findPendingTransactions + + var findPendingTransactionsLatestHeightOffsetLimitThrowableError: Error? + var findPendingTransactionsLatestHeightOffsetLimitCallsCount = 0 + var findPendingTransactionsLatestHeightOffsetLimitCalled: Bool { + return findPendingTransactionsLatestHeightOffsetLimitCallsCount > 0 + } + var findPendingTransactionsLatestHeightOffsetLimitReceivedArguments: (latestHeight: BlockHeight, offset: Int, limit: Int)? + var findPendingTransactionsLatestHeightOffsetLimitReturnValue: [ZcashTransaction.Overview]! + var findPendingTransactionsLatestHeightOffsetLimitClosure: ((BlockHeight, Int, Int) async throws -> [ZcashTransaction.Overview])? + + func findPendingTransactions(latestHeight: BlockHeight, offset: Int, limit: Int) async throws -> [ZcashTransaction.Overview] { + if let error = findPendingTransactionsLatestHeightOffsetLimitThrowableError { + throw error + } + findPendingTransactionsLatestHeightOffsetLimitCallsCount += 1 + findPendingTransactionsLatestHeightOffsetLimitReceivedArguments = (latestHeight: latestHeight, offset: offset, limit: limit) + if let closure = findPendingTransactionsLatestHeightOffsetLimitClosure { + return try await closure(latestHeight, offset, limit) + } else { + return findPendingTransactionsLatestHeightOffsetLimitReturnValue + } + } + + // MARK: - findReceived + + var findReceivedOffsetLimitThrowableError: Error? + var findReceivedOffsetLimitCallsCount = 0 + var findReceivedOffsetLimitCalled: Bool { + return findReceivedOffsetLimitCallsCount > 0 + } + var findReceivedOffsetLimitReceivedArguments: (offset: Int, limit: Int)? + var findReceivedOffsetLimitReturnValue: [ZcashTransaction.Overview]! + var findReceivedOffsetLimitClosure: ((Int, Int) async throws -> [ZcashTransaction.Overview])? + + func findReceived(offset: Int, limit: Int) async throws -> [ZcashTransaction.Overview] { + if let error = findReceivedOffsetLimitThrowableError { + throw error + } + findReceivedOffsetLimitCallsCount += 1 + findReceivedOffsetLimitReceivedArguments = (offset: offset, limit: limit) + if let closure = findReceivedOffsetLimitClosure { + return try await closure(offset, limit) + } else { + return findReceivedOffsetLimitReturnValue + } + } + + // MARK: - findSent + + var findSentOffsetLimitThrowableError: Error? + var findSentOffsetLimitCallsCount = 0 + var findSentOffsetLimitCalled: Bool { + return findSentOffsetLimitCallsCount > 0 + } + var findSentOffsetLimitReceivedArguments: (offset: Int, limit: Int)? + var findSentOffsetLimitReturnValue: [ZcashTransaction.Overview]! + var findSentOffsetLimitClosure: ((Int, Int) async throws -> [ZcashTransaction.Overview])? + + func findSent(offset: Int, limit: Int) async throws -> [ZcashTransaction.Overview] { + if let error = findSentOffsetLimitThrowableError { + throw error + } + findSentOffsetLimitCallsCount += 1 + findSentOffsetLimitReceivedArguments = (offset: offset, limit: limit) + if let closure = findSentOffsetLimitClosure { + return try await closure(offset, limit) + } else { + return findSentOffsetLimitReturnValue + } + } + + // MARK: - findMemos + + var findMemosForThrowableError: Error? + var findMemosForCallsCount = 0 + var findMemosForCalled: Bool { + return findMemosForCallsCount > 0 + } + var findMemosForReceivedTransaction: ZcashTransaction.Overview? + var findMemosForReturnValue: [Memo]! + var findMemosForClosure: ((ZcashTransaction.Overview) async throws -> [Memo])? + + func findMemos(for transaction: ZcashTransaction.Overview) async throws -> [Memo] { + if let error = findMemosForThrowableError { + throw error + } + findMemosForCallsCount += 1 + findMemosForReceivedTransaction = transaction + if let closure = findMemosForClosure { + return try await closure(transaction) + } else { + return findMemosForReturnValue + } + } + + // MARK: - getRecipients + + var getRecipientsForThrowableError: Error? + var getRecipientsForCallsCount = 0 + var getRecipientsForCalled: Bool { + return getRecipientsForCallsCount > 0 + } + var getRecipientsForReceivedId: Int? + var getRecipientsForReturnValue: [TransactionRecipient]! + var getRecipientsForClosure: ((Int) async throws -> [TransactionRecipient])? + + func getRecipients(for id: Int) async throws -> [TransactionRecipient] { + if let error = getRecipientsForThrowableError { + throw error + } + getRecipientsForCallsCount += 1 + getRecipientsForReceivedId = id + if let closure = getRecipientsForClosure { + return try await closure(id) + } else { + return getRecipientsForReturnValue + } + } + + // MARK: - getTransactionOutputs + + var getTransactionOutputsForThrowableError: Error? + var getTransactionOutputsForCallsCount = 0 + var getTransactionOutputsForCalled: Bool { + return getTransactionOutputsForCallsCount > 0 + } + var getTransactionOutputsForReceivedId: Int? + var getTransactionOutputsForReturnValue: [ZcashTransaction.Output]! + var getTransactionOutputsForClosure: ((Int) async throws -> [ZcashTransaction.Output])? + + func getTransactionOutputs(for id: Int) async throws -> [ZcashTransaction.Output] { + if let error = getTransactionOutputsForThrowableError { + throw error + } + getTransactionOutputsForCallsCount += 1 + getTransactionOutputsForReceivedId = id + if let closure = getTransactionOutputsForClosure { + return try await closure(id) + } else { + return getTransactionOutputsForReturnValue + } + } + +} +class UTXOFetcherMock: UTXOFetcher { + + + init( + ) { + } + + // MARK: - fetch + + var fetchAtDidFetchThrowableError: Error? + var fetchAtDidFetchCallsCount = 0 + var fetchAtDidFetchCalled: Bool { + return fetchAtDidFetchCallsCount > 0 + } + var fetchAtDidFetchReceivedArguments: (range: CompactBlockRange, didFetch: (Float) async -> Void)? + var fetchAtDidFetchReturnValue: (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])! + var fetchAtDidFetchClosure: ((CompactBlockRange, @escaping (Float) async -> Void) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))? + + func fetch(at range: CompactBlockRange, didFetch: @escaping (Float) async -> Void) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]) { + if let error = fetchAtDidFetchThrowableError { + throw error + } + fetchAtDidFetchCallsCount += 1 + fetchAtDidFetchReceivedArguments = (range: range, didFetch: didFetch) + if let closure = fetchAtDidFetchClosure { + return try await closure(range, didFetch) + } else { + return fetchAtDidFetchReturnValue + } + } + +} +class ZcashFileManagerMock: ZcashFileManager { + + + init( + ) { + } + + // MARK: - isReadableFile + + var isReadableFileAtPathCallsCount = 0 + var isReadableFileAtPathCalled: Bool { + return isReadableFileAtPathCallsCount > 0 + } + var isReadableFileAtPathReceivedPath: String? + var isReadableFileAtPathReturnValue: Bool! + var isReadableFileAtPathClosure: ((String) -> Bool)? + + func isReadableFile(atPath path: String) -> Bool { + isReadableFileAtPathCallsCount += 1 + isReadableFileAtPathReceivedPath = path + if let closure = isReadableFileAtPathClosure { + return closure(path) + } else { + return isReadableFileAtPathReturnValue + } + } + + // MARK: - removeItem + + var removeItemAtThrowableError: Error? + var removeItemAtCallsCount = 0 + var removeItemAtCalled: Bool { + return removeItemAtCallsCount > 0 + } + var removeItemAtReceivedURL: URL? + var removeItemAtClosure: ((URL) throws -> Void)? + + func removeItem(at URL: URL) throws { + if let error = removeItemAtThrowableError { + throw error + } + removeItemAtCallsCount += 1 + removeItemAtReceivedURL = URL + try removeItemAtClosure!(URL) + } + + // MARK: - isDeletableFile + + var isDeletableFileAtPathCallsCount = 0 + var isDeletableFileAtPathCalled: Bool { + return isDeletableFileAtPathCallsCount > 0 + } + var isDeletableFileAtPathReceivedPath: String? + var isDeletableFileAtPathReturnValue: Bool! + var isDeletableFileAtPathClosure: ((String) -> Bool)? + + func isDeletableFile(atPath path: String) -> Bool { + isDeletableFileAtPathCallsCount += 1 + isDeletableFileAtPathReceivedPath = path + if let closure = isDeletableFileAtPathClosure { + return closure(path) + } else { + return isDeletableFileAtPathReturnValue + } + } + } actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { @@ -614,7 +2193,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } decryptAndStoreTransactionTxBytesMinedHeightCallsCount += 1 decryptAndStoreTransactionTxBytesMinedHeightReceivedArguments = (txBytes: txBytes, minedHeight: minedHeight) - try await decryptAndStoreTransactionTxBytesMinedHeightClosure?(txBytes, minedHeight) + try await decryptAndStoreTransactionTxBytesMinedHeightClosure!(txBytes, minedHeight) } // MARK: - getBalance @@ -856,7 +2435,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } initAccountsTableUfvksCallsCount += 1 initAccountsTableUfvksReceivedUfvks = ufvks - try await initAccountsTableUfvksClosure?(ufvks) + try await initAccountsTableUfvksClosure!(ufvks) } // MARK: - initDataDb @@ -914,7 +2493,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } initBlocksTableHeightHashTimeSaplingTreeCallsCount += 1 initBlocksTableHeightHashTimeSaplingTreeReceivedArguments = (height: height, hash: hash, time: time, saplingTree: saplingTree) - try await initBlocksTableHeightHashTimeSaplingTreeClosure?(height, hash, time, saplingTree) + try await initBlocksTableHeightHashTimeSaplingTreeClosure!(height, hash, time, saplingTree) } // MARK: - listTransparentReceivers @@ -1038,7 +2617,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } validateCombinedChainLimitCallsCount += 1 validateCombinedChainLimitReceivedLimit = limit - try await validateCombinedChainLimitClosure?(limit) + try await validateCombinedChainLimitClosure!(limit) } // MARK: - rewindToHeight @@ -1063,7 +2642,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } rewindToHeightHeightCallsCount += 1 rewindToHeightHeightReceivedHeight = height - try await rewindToHeightHeightClosure?(height) + try await rewindToHeightHeightClosure!(height) } // MARK: - rewindCacheToHeight @@ -1088,7 +2667,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } rewindCacheToHeightHeightCallsCount += 1 rewindCacheToHeightHeightReceivedHeight = height - try await rewindCacheToHeightHeightClosure?(height) + try await rewindCacheToHeightHeightClosure!(height) } // MARK: - scanBlocks @@ -1113,7 +2692,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } scanBlocksLimitCallsCount += 1 scanBlocksLimitReceivedLimit = limit - try await scanBlocksLimitClosure?(limit) + try await scanBlocksLimitClosure!(limit) } // MARK: - putUnspentTransparentOutput @@ -1138,7 +2717,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } putUnspentTransparentOutputTxidIndexScriptValueHeightCallsCount += 1 putUnspentTransparentOutputTxidIndexScriptValueHeightReceivedArguments = (txid: txid, index: index, script: script, value: value, height: height) - try await putUnspentTransparentOutputTxidIndexScriptValueHeightClosure?(txid, index, script, value, height) + try await putUnspentTransparentOutputTxidIndexScriptValueHeightClosure!(txid, index, script, value, height) } // MARK: - shieldFunds @@ -1201,7 +2780,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { throw error } initBlockMetadataDbCallsCount += 1 - try await initBlockMetadataDbClosure?() + try await initBlockMetadataDbClosure!() } // MARK: - writeBlocksMetadata @@ -1226,7 +2805,7 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } writeBlocksMetadataBlocksCallsCount += 1 writeBlocksMetadataBlocksReceivedBlocks = blocks - try await writeBlocksMetadataBlocksClosure?(blocks) + try await writeBlocksMetadataBlocksClosure!(blocks) } // MARK: - latestCachedBlockHeight diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index dd35827b..8350bab0 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -209,7 +209,7 @@ extension SynchronizerState { syncSessionID: .nullID, shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)), transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)), - internalSyncStatus: .fetching(0), + internalSyncStatus: .syncing(0), latestScannedHeight: 111111, latestBlockHeight: 222222, latestScannedTime: 12345678 diff --git a/Tests/TestUtils/TestCoordinator.swift b/Tests/TestUtils/TestCoordinator.swift index c7bf7bc7..b7a68c5f 100644 --- a/Tests/TestUtils/TestCoordinator.swift +++ b/Tests/TestUtils/TestCoordinator.swift @@ -213,29 +213,26 @@ extension TestCoordinator { try await service.latestBlockHeight() } - func reset(saplingActivation: BlockHeight, branchID: String, chainName: String) throws { - Task { - await self.synchronizer.blockProcessor.stop() - let config = await self.synchronizer.blockProcessor.config + func reset(saplingActivation: BlockHeight, branchID: String, chainName: String) async throws { + await self.synchronizer.blockProcessor.stop() - let newConfig = CompactBlockProcessor.Configuration( - alias: config.alias, - fsBlockCacheRoot: config.fsBlockCacheRoot, - dataDb: config.dataDb, - spendParamsURL: config.spendParamsURL, - outputParamsURL: config.outputParamsURL, - saplingParamsSourceURL: config.saplingParamsSourceURL, - downloadBatchSize: config.downloadBatchSize, - retries: config.retries, - maxBackoffInterval: config.maxBackoffInterval, - rewindDistance: config.rewindDistance, - walletBirthdayProvider: config.walletBirthdayProvider, - saplingActivation: saplingActivation, - network: config.network - ) + let config = await self.synchronizer.blockProcessor.config + let newConfig = CompactBlockProcessor.Configuration( + alias: config.alias, + fsBlockCacheRoot: config.fsBlockCacheRoot, + dataDb: config.dataDb, + spendParamsURL: config.spendParamsURL, + outputParamsURL: config.outputParamsURL, + saplingParamsSourceURL: config.saplingParamsSourceURL, + retries: config.retries, + maxBackoffInterval: config.maxBackoffInterval, + rewindDistance: config.rewindDistance, + walletBirthdayProvider: config.walletBirthdayProvider, + saplingActivation: saplingActivation, + network: config.network + ) - await self.synchronizer.blockProcessor.update(config: newConfig) - } + await self.synchronizer.blockProcessor.update(config: newConfig) try service.reset(saplingActivation: saplingActivation, branchID: branchID, chainName: chainName) } diff --git a/docs/cbp_state_machine.puml b/docs/cbp_state_machine.puml new file mode 100644 index 00000000..574e201b --- /dev/null +++ b/docs/cbp_state_machine.puml @@ -0,0 +1,90 @@ +@startuml +hide empty description + +note as Lines + Green lines are happy paths. + Red lines are error paths. + Blue lines are stop paths. + + In general any action for any state can produce error. + And the sync process can be stopped during any action. +end note + + +[*] -> idle + +idle -[#green,bold]-> migrateLegacyCacheDB + +migrateLegacyCacheDB : MigrateLegacyCacheDBAction +migrateLegacyCacheDB -[#green,bold]-> validateServer +migrateLegacyCacheDB -[#red]-> failed : Error occured. +migrateLegacyCacheDB -[#blue]-> stopped : Sync was stopped. + +validateServer : ValidateServerAction +validateServer -[#green,bold]-> computeSyncRanges +validateServer -[#red]-> failed : Error occured. +validateServer -[#blue]-> stopped : Sync was stopped. + +computeSyncRanges : ComputeSyncRangesAction +computeSyncRanges -[#green,bold]-> checksBeforeSync +computeSyncRanges -[#red]-> failed : Error occured. +computeSyncRanges -[#blue]-> stopped : Sync was stopped. + +checksBeforeSync : ChecksBeforeSyncAction +checksBeforeSync -[#green,bold]-> fetchUTXO +checksBeforeSync -[#red]-> failed : Error occured. +checksBeforeSync -[#blue]-> stopped : Sync was stopped. + +fetchUTXO : FetchUTXOAction +fetchUTXO -[#green,bold]-> handleSaplingParams +fetchUTXO -[#red]-> failed : Error occured. +fetchUTXO -[#blue]-> stopped : Sync was stopped. + +handleSaplingParams : SaplingParamsAction +handleSaplingParams -[#green,bold]-> download +handleSaplingParams -[#red]-> failed : Error occured. +handleSaplingParams -[#blue]-> stopped : Sync was stopped. + +download : DownloadAction +download -[#green,bold]-> validate +download -[#red]-> failed : Error occured. +download -[#blue]-> stopped : Sync was stopped. + +validate : ValidateAction +validate -[#green,bold]-> scan +validate -[#red]-> failed : Error occured. +validate -[#blue]-> stopped : Sync was stopped. + +scan : ScanAction +scan -[#green,bold]-> clearAlreadyScannedBlocks +scan -[#red]-> failed : Error occured. +scan -[#blue]-> stopped : Sync was stopped. + +clearAlreadyScannedBlocks : ClearAlreadyScannedBlocksAction +clearAlreadyScannedBlocks -[#green,bold]-> enhance +clearAlreadyScannedBlocks -[#red]-> failed : Error occured. +clearAlreadyScannedBlocks -[#blue]-> stopped : Sync was stopped. + +enhance : EnhanceAction +enhance -[#green,bold]-> download : Not all blocks in the\nsync range are downloaded\nand scanned yet. +enhance -[#green,bold]-> clearCache : All the blocks in\nthe sync range are downloaded\nand scanned. +enhance -[#red]-> failed : Error occured. +enhance -[#blue]-> stopped : Sync was stopped. + +note right of enhance + Enhance transactions in batches of 1000 + blocks. Dont't do it for each scan batch + which is usualy 100 blocks. +end note + +clearCache : ClearCacheAction +clearCache --> finished +clearCache -[#red]-> failed : Error occured. +clearCache -[#blue]-> stopped : Sync was stopped. + +finished --> [*] +failed --> [*] +stopped --> [*] + +@enduml + diff --git a/docs/images/cbp_state_machine.png b/docs/images/cbp_state_machine.png new file mode 100644 index 00000000..3cfd8aad Binary files /dev/null and b/docs/images/cbp_state_machine.png differ