diff --git a/ZcashLightClientKit/Service/LightWalletGRPCService.swift b/ZcashLightClientKit/Service/LightWalletGRPCService.swift index 271e89d4..5f0157f3 100644 --- a/ZcashLightClientKit/Service/LightWalletGRPCService.swift +++ b/ZcashLightClientKit/Service/LightWalletGRPCService.swift @@ -90,8 +90,12 @@ struct BlockProgress: BlockProgressReporting { var progressHeight: BlockHeight } +extension Notification.Name { + static let connectionStatusChanged = Notification.Name("LightWalletServiceConnectivityStatusChanged") +} + public class LightWalletGRPCService { - + var queue: DispatchQueue let channel: Channel let connectionDelegate: ConnectionStatusManager @@ -488,5 +492,12 @@ extension LightWalletServiceError { class ConnectionStatusManager: ConnectivityStateDelegate { func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) { LoggerProxy.event("Connection Changed from \(oldState) to \(newState)") + NotificationCenter.default.post( + name: .blockProcessorConnectivityStateChanged, + object: self, + userInfo: [ + CompactBlockProcessorNotificationKey.currentConnectivityStatus : newState, + CompactBlockProcessorNotificationKey.previousConnectivityStatus : oldState + ]) } } diff --git a/ZcashLightClientKit/Synchronizer.swift b/ZcashLightClientKit/Synchronizer.swift index e393c3cd..ba3ccd37 100644 --- a/ZcashLightClientKit/Synchronizer.swift +++ b/ZcashLightClientKit/Synchronizer.swift @@ -48,6 +48,32 @@ extension ShieldFundsError: LocalizedError { } } +/** + Represent the connection state to the lightwalletd server + */ +public enum ConnectionState { + /** + not in use + */ + case idle + /** + there's a connection being attempted from a non error state + */ + case connecting + /** + connection is established, ready to use or in use + */ + case online + /** + the connection is being re-established after losing it temporarily + */ + case reconnecting + /** + the connection has been closed + */ + case shutdown +} + /** Primary interface for interacting with the SDK. Defines the contract that specific @@ -57,17 +83,9 @@ implementations like SdkSynchronizer fulfill. public protocol Synchronizer { /** - Value representing the Status of this Synchronizer. As the status changes, a new - value will be emitted by KVO + Value representing the Status of this Synchronizer. As the status changes, it will be also notified */ - var status: Status { get } - - /** - A flow of progress values, typically corresponding to this Synchronizer downloading blocks. - Typically, any non-zero value below 1.0 indicates that progress indicators can be shown and - a value of 1.0 signals that progress is complete and any progress indicators can be hidden. KVO Compliant - */ - var progress: Float { get } + var status: SyncStatus { get } /** prepares this initializer to operate. Initializes the internal state with the given Extended Viewing Keys and a wallet birthday found in the initializer object @@ -215,73 +233,65 @@ public protocol Synchronizer { func rewind(_ policy: RewindPolicy) throws } -/** - The Status of the synchronizer - */ -public enum Status { - +public enum SyncStatus: Equatable { + /** Indicates that this Synchronizer is actively preparing to start, which usually involves setting up database tables, migrations or taking other maintenance steps that need to occur after an upgrade. */ case unprepared - /** - Indicates that [stop] has been called on this Synchronizer and it will no longer be used. - */ - case stopped - - /** - Indicates that this Synchronizer is disconnected from its lightwalletd server. - When set, a UI element may want to turn red. - */ - case disconnected - /** Indicates that this Synchronizer is actively downloading new blocks from the server. */ - case downloading - + case downloading(_ status: BlockProgressReporting) /** Indicates that this Synchronizer is actively validating new blocks that were downloaded from the server. Blocks need to be verified before they are scanned. This confirms that each block is chain-sequential, thereby detecting missing blocks and reorgs. */ case validating - /** Indicates that this Synchronizer is actively scanning new valid blocks that were downloaded from the server. */ - case scanning - + case scanning(_ progress: BlockProgressReporting) /** Indicates that this Synchronizer is actively enhancing newly scanned blocks with additional transaction details, fetched from the server. */ - case enhancing - + case enhancing(_ progress: EnhancementProgress) /** fetches the transparent balance and stores it locally */ case fetching - /** 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 + /** + Indicates that [stop] has been called on this Synchronizer and it will no longer be used. + */ + case stopped + /** + Indicates that this Synchronizer is disconnected from its lightwalletd server. + When set, a UI element may want to turn red. + */ + case disconnected + case error(_ error: Error) public var isSyncing: Bool { switch self { - case .disconnected, .synced, .synced, .unprepared: - return false - default: + case .downloading, .validating, .scanning, .enhancing, .fetching: return true + default: + return false } } } + /** Kind of transactions handled by a Synchronizer */ @@ -304,3 +314,73 @@ public enum RewindPolicy { case transaction(_ transaction: TransactionEntity) case quick } + + + + +extension SyncStatus { + public static func == (lhs: SyncStatus, rhs: SyncStatus) -> Bool { + switch lhs { + case .unprepared: + if case .unprepared = rhs { + return true + } else { + return false + } + case .disconnected: + if case .disconnected = rhs { + return true + } else { + return false + } + case .downloading: + if case .downloading = rhs { + return true + } else { + return false + } + case .validating: + if case .validating = rhs { + return true + } else { + return false + } + case .scanning: + if case .scanning = rhs { + return true + } else { + return false + } + case .enhancing: + if case .enhancing = rhs { + return true + } else { + return false + } + case .fetching: + if case .fetching = rhs { + return true + } else { + return false + } + case .synced: + if case .synced = rhs { + return true + } else { + return false + } + case .stopped: + if case .stopped = rhs { + return true + } else { + return false + } + case .error: + if case .error = rhs { + return true + } else { + return false + } + } + } +} diff --git a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift index 28a9daa6..bb028be0 100644 --- a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift +++ b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift @@ -91,6 +91,8 @@ public extension Notification.Name { - Note: query userInfo on NotificationKeys.error for an error */ static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed") + + static let synchronizerConnectionStateChanged = Notification.Name("SynchronizerConnectionStateChanged") } /** @@ -107,9 +109,11 @@ public class SDKSynchronizer: Synchronizer { public static let error = "SDKSynchronizer.error" public static let currentStatus = "SDKSynchronizer.currentStatus" public static let nextStatus = "SDKSynchronizer.nextStatus" + public static let currentConnectionState = "SDKSynchronizer.currentConnectionState" + public static let previousConnectionState = "SDKSynchronizer.previousConnectionState" } - public private(set) var status: Status { + public private(set) var status: SyncStatus { didSet { notify(status: status) } @@ -140,7 +144,7 @@ public class SDKSynchronizer: Synchronizer { } - init(status: Status, + init(status: SyncStatus, initializer: Initializer, transactionManager: OutboundTransactionManager, transactionRepository: TransactionRepository, @@ -187,7 +191,7 @@ public class SDKSynchronizer: Synchronizer { assert(true,"warning: synchronizer started when already started") // TODO: remove this assertion sometime in the near future LoggerProxy.debug("warning: synchronizer started when already started") return - case .stopped, .synced,.disconnected: + case .stopped, .synced,.disconnected, .error: do { try blockProcessor.start(retry: retry) } catch { @@ -269,9 +273,30 @@ public class SDKSynchronizer: Synchronizer { name: Notification.Name.blockProcessorFoundTransactions, object: processor) + center.addObserver(self, + selector: #selector(connectivityStateChanged(_:)), + name: Notification.Name.blockProcessorConnectivityStateChanged, + object: processor) } // MARK: Block Processor notifications + @objc func connectivityStateChanged(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let previous = userInfo[CompactBlockProcessorNotificationKey.previousConnectivityStatus] as? ConnectivityState, + let current = userInfo[CompactBlockProcessorNotificationKey.currentConnectivityStatus] as? ConnectivityState else { + LoggerProxy.error("found \(Notification.Name.blockProcessorConnectivityStateChanged) but lacks dictionary information. this is probably a programming error") + return + } + + NotificationCenter.default.post( + name: .synchronizerConnectionStateChanged, + object: self, + userInfo: [ + NotificationKeys.previousConnectionState : ConnectionState(previous), + NotificationKeys.currentConnectionState : ConnectionState(current) + ]) + + } @objc func transactionsFound(_ notification: Notification) { guard let userInfo = notification.userInfo, @@ -310,41 +335,41 @@ public class SDKSynchronizer: Synchronizer { @objc func processorStartedDownloading(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.status = .downloading + guard let self = self, self.status != .downloading(NullProgress()) else { return } + self.status = .downloading(NullProgress()) } } @objc func processorStartedValidating(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + guard let self = self, self.status != .validating else { return } self.status = .validating } } @objc func processorStartedScanning(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.status = .scanning + guard let self = self, self.status != .scanning(NullProgress()) else { return } + self.status = .scanning(NullProgress()) } } @objc func processorStartedEnhancing(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.status = .enhancing + guard let self = self, self.status != .enhancing(NullEnhancementProgress()) else { return } + self.status = .enhancing(NullEnhancementProgress()) } } @objc func processorStartedFetching(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + guard let self = self, self.status != .fetching else { return } self.status = .fetching } } @objc func processorStopped(_ notification: Notification) { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + guard let self = self, self.status != .stopped else { return } self.status = .stopped } } @@ -355,10 +380,11 @@ public class SDKSynchronizer: Synchronizer { guard let self = self else { return } if let error = notification.userInfo?[CompactBlockProcessorNotificationKey.error] as? Error { self.notifyFailure(error) + self.status = .error(self.mapError(error)) } else { self.notifyFailure(CompactBlockProcessorError.generalError(message: "This is strange. processorFailed Call received no error message")) + self.status = .error(SynchronizerError.generalError(message: "This is strange. processorFailed Call received no error message")) } - self.status = .disconnected } } @@ -619,7 +645,7 @@ public class SDKSynchronizer: Synchronizer { NotificationCenter.default.post(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: userInfo) } - private func notifyStatusChange(newValue: Status, oldValue: Status) { + private func notifyStatusChange(newValue: SyncStatus, oldValue: SyncStatus) { NotificationCenter.default.post(name: .synchronizerStatusWillUpdate, object: self, userInfo: @@ -627,7 +653,7 @@ public class SDKSynchronizer: Synchronizer { NotificationKeys.nextStatus : newValue ]) } - private func notify(status: Status) { + private func notify(status: SyncStatus) { switch status { case .disconnected: @@ -649,6 +675,8 @@ public class SDKSynchronizer: Synchronizer { NotificationCenter.default.post(name: Notification.Name.synchronizerEnhancing, object: self) case .fetching: NotificationCenter.default.post(name: Notification.Name.synchronizerFetching, object: self) + case .error(let e): + self.notifyFailure(e) } } // MARK: book keeping @@ -760,3 +788,44 @@ extension SDKSynchronizer { (try? self.allReceivedTransactions()) ?? [ConfirmedTransactionEntity]() } } + +import GRPC +extension ConnectionState { + init(_ connectivityState: ConnectivityState) { + switch connectivityState { + case .connecting: + self = .connecting + case .idle: + self = .idle + case .ready: + self = .online + case .shutdown: + self = .shutdown + case .transientFailure: + self = .reconnecting + } + } +} + + + +fileprivate struct NullEnhancementProgress: EnhancementProgress { + var totalTransactions: Int { 0 } + var enhancedTransactions: Int { 0 } + var lastFoundTransaction: ConfirmedTransactionEntity? { nil } + var range: CompactBlockRange { 0 ... 0 } +} + +fileprivate struct NullProgress: BlockProgressReporting { + var startHeight: BlockHeight { + 0 + } + + var targetHeight: BlockHeight { + 0 + } + + var progressHeight: BlockHeight { + 0 + } +}